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
5 changes: 2 additions & 3 deletions .spi.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
# Swift Package Index: which modules get hosted DocC documentation. The package declares macOS 15 / iOS 18 (the shipped
# libhdf5 is arm64 macOS), so the docs build on the Index's macOS SwiftPM builder. FormatoMSA is left out until it holds API
# (it is an empty placeholder today).
# libhdf5 is arm64 macOS), so the docs build on the Index's macOS SwiftPM builder. Every product with a .docc catalogue is listed.
version: 1
builder:
configs:
- platform: macos-spm
documentation_targets: [FormatoCore, FormatoDM, FormatoEMD, FormatoHDF5, FormatoTIFF, FormatoMRC, FormatoEMPAD, FormatoMIB]
documentation_targets: [Formato, FormatoCore, FormatoDM, FormatoEMD, FormatoHDF5, FormatoTIFF, FormatoMRC, FormatoEMPAD, FormatoMIB, FormatoMSA, FormatoPeek]
3 changes: 2 additions & 1 deletion CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ abstract: >-
authors:
- family-names: Lobpreis
given-names: Paul
# TODO(Paul): add orcid: "https://orcid.org/…" and affiliation: "…" here
version: "0.11.0"
date-released: "2026-10-22"
license: GPL-3.0
repository-code: "https://github.com/mac4DSTEM/FormatoKit"
keywords:
Expand Down
33 changes: 33 additions & 0 deletions Licenses/Pint-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
Copyright (c) 2012 by Hernan E. Grecco and contributors. See AUTHORS
for more details.

Some rights reserved.

Redistribution and use in source and binary forms of the software as well
as documentation, with or without modification, are permitted provided
that the following conditions are met:

* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.

* Redistributions in binary form must reproduce the above
copyright notice, this list of conditions and the following
disclaimer in the documentation and/or other materials provided
with the distribution.

* The names of the contributors may not be used to endorse or
promote products derived from this software without specific
prior written permission.

THIS SOFTWARE AND DOCUMENTATION IS PROVIDED BY THE COPYRIGHT HOLDERS AND
CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT
NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER
OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE AND DOCUMENTATION, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
DAMAGE.
30 changes: 30 additions & 0 deletions Licenses/tifffile-LICENSE.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
BSD-3-Clause license

Copyright (c) 2008-2026, Christoph Gohlke
All rights reserved.

Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:

1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.

2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.

3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.

THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
6 changes: 3 additions & 3 deletions NOTICE
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ FormatoCancellation.swift, FormatoFileName.swift come from SpectroKit 0.3.0 (tag
commit 14c6acf8), itself extracted from mac4DSTEM (commit 5ba712a0); the tests
under Tests/FormatoDMTests, FormatoEMDTests, FormatoHDF5Tests and
FormatoHDF5ExitProbe, and the fixtures listed in Tests/README.md, come from the
same tag. The design note listing every change is archived in the suite's private folder.
same tag.

SpectroKit - https://github.com/mac4DSTEM/SpectroKit
mac4DSTEM - https://github.com/mac4DSTEM/mac4DSTEM
Expand Down Expand Up @@ -74,7 +74,7 @@ from the pinned clone and not committed.

tifffile - https://github.com/cgohlke/tifffile, version 2026.9.20
Copyright (c) 2008-2026, Christoph Gohlke
BSD 3-Clause License
BSD 3-Clause License: Licenses/tifffile-LICENSE.txt
macTEM - https://github.com/mac4DSTEM/macTEM, GNU General Public License v3.0


Expand All @@ -89,7 +89,7 @@ tools/step-1/pint_factors.py.

Pint - https://github.com/hgrecco/pint, version 0.26.1
Copyright (c) 2012 by Hernan E. Grecco and contributors
BSD 3-Clause License
BSD 3-Clause License: Licenses/Pint-LICENSE.txt


================================================================================
Expand Down
26 changes: 16 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

# FormatoKit

[![CI](https://github.com/mac4DSTEM/FormatoKit/actions/workflows/ci.yml/badge.svg)](https://github.com/mac4DSTEM/FormatoKit/actions/workflows/ci.yml)
[![Docs](https://github.com/mac4DSTEM/FormatoKit/actions/workflows/docs.yml/badge.svg)](https://mac4dstem.github.io/FormatoKit/documentation/)
[![Release](https://img.shields.io/github/v/tag/mac4DSTEM/FormatoKit?label=release&sort=semver)](https://github.com/mac4DSTEM/FormatoKit/tags)
[![Licence: GPL-3.0](https://img.shields.io/badge/licence-GPL--3.0-blue)](LICENSE)

A file-format library for macOS: the Mac counterpart of [RosettaSciIO](https://github.com/hyperspy/rosettasciio).
A Swift package of readers (and later writers) for the electron-microscopy formats, one module per format over one data model,
A Swift package of readers and writers for the electron-microscopy formats, one module per format over one data model,
checked bit for bit against RosettaSciIO at commit `049e7d70` (its signals, axes, units and metadata; where it raises, so
does FormatoKit, by name). No UI. Import it by tagged version. Swift 6 strict concurrency, headless, lazy where a file is
large, one SwiftPM product per module.
Expand All @@ -13,7 +18,7 @@ large, one SwiftPM product per module.
| `FormatoCore` | the data model: arrays as stored, axes, units (Pint's compact units, ported), metadata with provenance, lazy signals; cancellation; `FormatoPNG` (PNG with text chunks) | reads; `AcquisitionMetadata`, `GuardedXML`, `FormatoFileWrite` |
| `FormatoDM` | Gatan DigitalMicrograph `.dm3` / `.dm4` (little-endian): every object as a signal, the tag tree, EDS and EELS spectrum images; complex, RGB and bool images are refused by name | reads; `AcquisitionMetadata.dm` |
| `FormatoEMD` | Thermo Fisher Velox `.emd`: images as signals in their stored type, titled as RosettaSciIO titles them; EDS and EELS spectrum images as signals (`VeloxSpectrumImageSignals`, with `VeloxSpectrumImageOptions` for RosettaSciIO's six options); the sparse spectrum streams; single spectra (`VeloxSpectrumSignals`, 0.9.0, "DevelopersKit" files included). Not read: NCEM / Berkeley EMD | reads; `AcquisitionMetadata.velox` |
| `FormatoHDF5` | `HDF5Datacube`: lazy rank-four numeric HDF5 / py4DSTEM EMD 1.0 hyperslabs in stored order, calibration axes, scalar metadata and lazy origin maps; the shared loader, lock and quit barrier. HSPY is not read. `BraggVectorsEMD`: write and read py4DSTEM BraggVectors (EMD 1.0 peak grid, shapes, calibration scalars), atomic publish, a hook for the caller's own attributes. EMD bundle writers: `EMDDatacubeWriter`, calibration and result maps on a write session, `HDF5NewFile` (design note, archived in the suite's private folder) | reads and writes, pinned; `HDF5RootAttributes` (sidecar file layer: write, read, publish) |
| `FormatoHDF5` | `HDF5Datacube`: lazy rank-four numeric HDF5 / py4DSTEM EMD 1.0 hyperslabs in stored order, calibration axes, scalar metadata and lazy origin maps; the shared loader, lock and quit barrier. HSPY is not read. `BraggVectorsEMD`: write and read py4DSTEM BraggVectors (EMD 1.0 peak grid, shapes, calibration scalars), atomic publish, a hook for the caller's own attributes. EMD bundle writers: `EMDDatacubeWriter`, calibration and result maps on a write session, `HDF5NewFile` | reads and writes, pinned; `HDF5RootAttributes` (sidecar file layer: write, read, publish) |
| `FormatoTIFF` | the TIFF container with a public tag walker (any private tag as stored); FEI, Gatan DM, ImageJ, tifffile-shaped and plain TIFF as signals. Compression, tiles and the other vendors' branches are refused by name | reads; `AcquisitionMetadata.tiff` (Velox XML, FEI INI, DM stream) |
| `FormatoMRC` | MRC / MRC2014: `MRCReader` (lazy images and stacks as RosettaSciIO reads them), `MRCAppImage` (the consumer's Float32 reading), acquisition metadata (RosettaSciIO's 128-byte FEI sections, or the FEI1/FEI2 extended-header record) | reads, pinned |
| `FormatoEMPAD` | EMPAD XML + raw, memory-mapped scan / image series with calibrations; 4D HDF5 later | reads, pinned |
Expand All @@ -23,13 +28,13 @@ large, one SwiftPM product per module.
| `FormatoPeek` | `Peek.thumbnailFrame(url:maxPixels:)` and `Peek.summary(url:)`: one decimated frame and the header facts of a DM, Velox, TIFF, MRC, MSA or py4DSTEM HDF5 file, never more than one frame read (for Quick Look and Finder previews) | 0.10.0 |

`docs/parity.md` is the checklist against RosettaSciIO's formats table and its reader options: what reads, what is pinned,
what is next.
what is next. The API reference (DocC, one page per module) is at <https://mac4dstem.github.io/FormatoKit/documentation/>.

## The first ten minutes

Add the package to your `Package.swift`, from the 0.4.0 tag of the FormatoKit repository
(`.package(url: <the repository URL>, from: "0.4.0")`, or `.package(path: "../FormatoKit")` for a clone), and the products
you need (`FormatoDM`, `FormatoEMD`, `FormatoTIFF`; each brings `FormatoCore`).
Add the package to your `Package.swift`
(`.package(url: "https://github.com/mac4DSTEM/FormatoKit.git", from: "0.11.0")`, or `.package(path: "../FormatoKit")` for a
clone), and the products you need (`FormatoDM`, `FormatoEMD`, `FormatoTIFF`; each brings `FormatoCore`).

To try it on files you have in hand, use the ones in this repository: planted files made for the tests, under
`Tests/*/Fixtures`. The snippets below name them relative to a clone's root, so run them from there. RosettaSciIO's own
Expand Down Expand Up @@ -128,7 +133,8 @@ attributes to the file, root or BraggVectors group.

macOS 15 or later on Apple silicon, iOS 18 or later, Swift 6.2 tools (Xcode 26 or later). The pure-Swift modules (FormatoCore, FormatoDM,
FormatoTIFF, FormatoMRC/EMPAD/MSA) build for iOS 18. FormatoHDF5 and FormatoEMD load the shipped arm64 macOS libhdf5 at run time: they
are macOS-only in practice (on iOS they compile, and a Velox read refuses).
are macOS-only in practice (on iOS they compile, and a Velox read refuses). The shipped binaries are arm64 only: an Intel
Mac cannot load them.
**libhdf5 is inside the package** (HDF5 2.1.1 with libsz and libaec, arm64, as three binary-target XCFrameworks that Xcode embeds and signs in an app), so `swift build`
and `swift test` need nothing installed. The Velox reader looks for libhdf5 in this order: the path in the
`FORMATO_HDF5_PATH` environment variable, the host app's `Frameworks` folder, the shipped copy (the `FormatoLibHDF5` framework), then the loader's path.
Expand All @@ -137,14 +143,14 @@ and `swift test` need nothing installed. The Velox reader looks for libhdf5 in t

GPL-3.0 (`LICENSE`, `NOTICE`). The DM and Velox readers are inherited from an earlier GPL-3.0 package (named in `NOTICE`). The signal adapters port **RosettaSciIO**'s rules (GPL-3.0-or-later, the HyperSpy developers), which is also the
oracle every reader is checked against; `FormatoUnits` ports what RosettaSciIO does with **Pint** (BSD-3-Clause) and carries
Pint's conversion factors. HDF5, libaec and libsz are BSD-style (`Licenses/`). One GPL part makes the package GPL-3.0: an app
Pint's conversion factors. HDF5, libaec and libsz are BSD-style; tifffile, whose TIFF rules FormatoTIFF ports, is BSD-3-Clause. Every third-party
licence text is in `Licenses/`. One GPL part makes the package GPL-3.0: an app
built on it must be GPL-compatible.

## Build and test

```sh
tools/test.sh # the gate: swift test -c release, warning-checked
tools/step-1/names.sh # the 121 inherited test names are all here
tools/consumer-check/run.sh # two separate packages, public API only, Debug, no warnings, libhdf5 from the package;
# the second runs this README's snippets
```
Expand All @@ -161,7 +167,7 @@ matches it. Tags are made by the maintainer; what each tag changes for a consume
identity); checked only through `swift test` and the consumer check.
- Big-endian DM files: none exists to test; not detected.
- Velox: `lazy=True` is not ported; hostile grids and bin counts were run on FormatoKit only, not
refuted against RosettaSciIO. Registered deviations are listed in `docs/parity.md`.
refuted against RosettaSciIO. Deliberate deviations are listed in `docs/parity.md`.
- Performance: `docs/benchmarks.md`. DM, TIFF and the 2048² Velox image read 1.2 to 2x slower than RosettaSciIO at typical sizes (`docs/open-items.md`); MIB, MSA, MRC, EMPAD, HDF5 4D and the Velox spectrum-image decode are at parity or faster.
- TIFF: OME and SubIFD series, generic pyramids, compression and the other vendor branches are refused, not read (tifffile's shaped series are read since 0.11.0).
- Validation: none. Agreement with RosettaSciIO and recovery of planted files are what is claimed.
33 changes: 33 additions & 0 deletions Sources/Formato/Formato.docc/Formato.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# ``Formato``

One entry point for every format FormatoKit reads: find out what a file is, then load its signals.

## Overview

``FormatoLoader/sniff(path:)`` names a file's format from its extension, confirmed by its first bytes (the TIFF and HDF5
signatures, DM's version word, Velox's `Version` dataset); a file whose extension and bytes disagree is
``FormatoFileKind/unknown``. ``FormatoLoader/load(path:)`` returns the signals of a DM, Velox, TIFF, MRC or MSA file the way
that format's own module returns them, one result per object, so one unreadable object does not hide the others.

```swift
import Formato

for result in try FormatoLoader.load(path: "image.dm4") {
switch result {
case .success(let lazy): print(lazy.title, lazy.array.shape)
case .failure(let error): print("refused:", error)
}
}
```

Formats whose data are 4D datacubes rather than signals (EMPAD, Merlin MIB, py4DSTEM HDF5) are named, not loaded:
``FormatoLoadError/useDedicatedReader(_:_:)`` points to the reader with its own options (`EMPADReader`, `MIBReader`,
`HDF5Datacube`).

## Topics

### Loading

- ``FormatoLoader``
- ``FormatoFileKind``
- ``FormatoLoadError``
30 changes: 30 additions & 0 deletions Sources/FormatoMIB/FormatoMIB.docc/FormatoMIB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# ``FormatoMIB``

Reads Quantum Detectors Merlin `.mib` files as lazy, memory-mapped FormatoCore signals.

## Overview

``MIBReader`` opens a MIB file and its optional `.hdr` companion and reads the frame headers but no detector payload; the
signal reads pixels on demand. Processed U08 (1- and 6-bit), U16 (12-bit) and U32 (24-bit) frames are read; packed R64 is
refused, as in both pinned readers.

```swift
import FormatoMIB

let reader = try MIBReader(path: "scan.mib")
let signal = reader.signal
print(reader.pixelCode, signal.array.shape)
```

``MIBConvention`` picks the reference behaviour. ``MIBConvention/rosettaSciIO`` (the default) follows RosettaSciIO 049e7d70:
the scan shape from the trigger and timestamps, squeezed axes, the penultimate axis flipped, the stored integer type.
``MIBConvention/appScanXY`` follows the original application's reader: the ScanX / ScanY companion, Float32 values, not
flipped. Keep the file unchanged while the reader or its signal exists.

## Topics

### Reading

- ``MIBReader``
- ``MIBConvention``
- ``MIBError``
24 changes: 22 additions & 2 deletions Sources/FormatoMRC/FormatoMRC.docc/FormatoMRC.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,33 @@
# ``FormatoMRC``

Reads MRC and MRC2014 volumes and stacks. The module has no public API yet: its format is not registered.
Reads MRC and MRC2014 images and stacks as RosettaSciIO's `mrc` plugin reads them.

## Overview

The module is empty until its format is registered and its readers are written. The article <doc:Scope> says what it will read.
``MRCReader`` opens a file lazily (the header and nothing else) and returns a FormatoCore signal: shape (NZ, NY, NX) with
every size-1 axis squeezed away, axes z / y / x in nanometres, the data in its stored type and byte order. Modes 0, 1, 2
and 6 are read; modes 4 (complex64) and 12 (float16) are refused by name with ``MRCError``.

```swift
import FormatoMRC

let reader = try MRCReader(path: "image.mrc")
let signal = try reader.signal.load()
print(signal.data.shape, signal.axes.map(\.units))
```

``MRCAppImage`` is the named option beside it: one MRC2014 image read as Float32 pixels with an Angstrom calibration, the
way the first consuming app reads it. `AcquisitionMetadata.mrc(path:convention:)` gives the acquisition facts of RosettaSciIO's
128-byte FEI sections or of the FEI1/FEI2 extended-header record. The article <doc:Scope> says what is pinned.

## Topics

### Reading

- ``MRCReader``
- ``MRCAppImage``
- ``MRCError``

### Scope

- <doc:Scope>
21 changes: 16 additions & 5 deletions Sources/FormatoMRC/FormatoMRC.docc/Scope.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,22 @@
# Scope

FormatoMRC is empty until its format is registered. This article says what it will read.
What FormatoMRC reads, and what it is checked against.

## What it will read
## What it reads

MRC and MRC2014 volumes and stacks, as RosettaSciIO's `mrc` plugin reads them: the header's cell, mode and extended header, FEI's extended header among them. The data is memory-mapped and read lazily. Writing comes after reading.
MRC and MRC2014 images and stacks, as RosettaSciIO 049e7d70's `mrc` plugin (`file_reader`) reads them: the standard header
read little-endian whatever the machine stamp says, the data after the extended header, modes 0 (uint8, as RosettaSciIO
reads it), 1, 2 and 6. Scales are the cell length over the sampling, in nanometres; a Thermo Fisher (FEI) extended header
gives the pixel size instead. Reads are lazy and memory-mapped.

## Status
## Not read

The module has no public types yet. Its format is registered before any code is written, and its public types are added then. The checks will be against RosettaSciIO's MRC test files.
DE movie files, virtual or external images, and modes 4 (complex64) and 12 (float16), which are refused by name. Writing is
not ported.

## Checks

Data bytes, type, shape and axes are pinned exactly against RosettaSciIO's reading of files written byte by byte for the
tests (`tools/acq-metadata/generate.py`).
``MRCAppImage`` is pinned against the consuming app's reader and against mrcfile's `voxel_size`. `docs/parity.md` in the
repository lists each function and its state.
2 changes: 2 additions & 0 deletions Sources/FormatoMRC/MRCReader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,9 @@
import Foundation
import FormatoCore

/// An MRC or MRC2014 file read as RosettaSciIO reads it: one lazy signal, size-1 axes squeezed, axes in nanometres.
public nonisolated struct MRCReader: Sendable {
/// The file's values, axes and metadata; nothing past the header is read until it is loaded or sliced.
public let signal: FormatoLazySignal

/// Opens the file at `path` lazily (the header and nothing else is read).
Expand Down
Loading
Loading