diff --git a/.spi.yml b/.spi.yml index 83f8aea..e186b07 100644 --- a/.spi.yml +++ b/.spi.yml @@ -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] diff --git a/CITATION.cff b/CITATION.cff index 85ed6ab..85b37bd 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -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: diff --git a/Licenses/Pint-LICENSE.txt b/Licenses/Pint-LICENSE.txt new file mode 100644 index 0000000..49aec52 --- /dev/null +++ b/Licenses/Pint-LICENSE.txt @@ -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. diff --git a/Licenses/tifffile-LICENSE.txt b/Licenses/tifffile-LICENSE.txt new file mode 100644 index 0000000..d8b9580 --- /dev/null +++ b/Licenses/tifffile-LICENSE.txt @@ -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. diff --git a/NOTICE b/NOTICE index 5edb05a..05faf0f 100644 --- a/NOTICE +++ b/NOTICE @@ -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 @@ -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 @@ -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 ================================================================================ diff --git a/README.md b/README.md index c17ab82..bdb7d67 100644 --- a/README.md +++ b/README.md @@ -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. @@ -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 | @@ -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 . ## The first ten minutes -Add the package to your `Package.swift`, from the 0.4.0 tag of the FormatoKit repository -(`.package(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 @@ -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. @@ -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 ``` @@ -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. diff --git a/Sources/Formato/Formato.docc/Formato.md b/Sources/Formato/Formato.docc/Formato.md new file mode 100644 index 0000000..aeeac07 --- /dev/null +++ b/Sources/Formato/Formato.docc/Formato.md @@ -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`` diff --git a/Sources/FormatoMIB/FormatoMIB.docc/FormatoMIB.md b/Sources/FormatoMIB/FormatoMIB.docc/FormatoMIB.md new file mode 100644 index 0000000..3c58f78 --- /dev/null +++ b/Sources/FormatoMIB/FormatoMIB.docc/FormatoMIB.md @@ -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`` diff --git a/Sources/FormatoMRC/FormatoMRC.docc/FormatoMRC.md b/Sources/FormatoMRC/FormatoMRC.docc/FormatoMRC.md index 401ae4e..12c0847 100644 --- a/Sources/FormatoMRC/FormatoMRC.docc/FormatoMRC.md +++ b/Sources/FormatoMRC/FormatoMRC.docc/FormatoMRC.md @@ -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 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 says what is pinned. ## Topics +### Reading + +- ``MRCReader`` +- ``MRCAppImage`` +- ``MRCError`` + ### Scope - diff --git a/Sources/FormatoMRC/FormatoMRC.docc/Scope.md b/Sources/FormatoMRC/FormatoMRC.docc/Scope.md index 84ab5a2..e2a9731 100644 --- a/Sources/FormatoMRC/FormatoMRC.docc/Scope.md +++ b/Sources/FormatoMRC/FormatoMRC.docc/Scope.md @@ -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. diff --git a/Sources/FormatoMRC/MRCReader.swift b/Sources/FormatoMRC/MRCReader.swift index 74a1564..54e5451 100644 --- a/Sources/FormatoMRC/MRCReader.swift +++ b/Sources/FormatoMRC/MRCReader.swift @@ -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). diff --git a/Sources/FormatoPeek/FormatoPeek.docc/FormatoPeek.md b/Sources/FormatoPeek/FormatoPeek.docc/FormatoPeek.md new file mode 100644 index 0000000..5840ae4 --- /dev/null +++ b/Sources/FormatoPeek/FormatoPeek.docc/FormatoPeek.md @@ -0,0 +1,28 @@ +# ``FormatoPeek`` + +A thumbnail and the header facts of a file, without reading the file: for Quick Look and Finder-style previews. + +## Overview + +``Peek/thumbnailFrame(url:maxPixels:)`` returns one decimated frame of a DM, Velox, TIFF, MRC, MSA or py4DSTEM HDF5 file. +Never more than one frame is read: a stack or a 4D cube is entered at its centre, an image is read at most one row at a time +(every `stride`-th row), and columns are decimated in memory. The pixels are the file's own, point sampled, never averaged. +``Peek/summary(url:)`` returns the facts a preview card shows: format, size, scan shape, pixel size, high tension, +magnification, camera length, instrument, detector and acquisition time, as the format's reader gives them. + +```swift +import FormatoPeek + +let frame = try Peek.thumbnailFrame(url: url, maxPixels: 256) +let card = try Peek.summary(url: url) +print(frame.width, frame.height, frame.kind, card.format, card.highTensionKV ?? 0) +``` + +``Peek/Kind`` tells a consumer how to stretch the frame: images by percentile, diffraction patterns in log scale. EMPAD and +Merlin MIB files are refused by name with ``Peek/PeekError``. + +## Topics + +### Previews + +- ``Peek``