Checkpoint committed by the coordinator, not by the authoring agent: the session hit its API limit mid-task and left this work uncommitted. Committed so it survives, NOT because it is finished - expect failing tests and half-applied changes. The agent resumes from here.
107 lines
4.9 KiB
Rust
107 lines
4.9 KiB
Rust
//! TRACES: FR-EXP-8
|
|
//! What an export is allowed to say about where it came from.
|
|
//!
|
|
//! # An allowlist, not a filter
|
|
//!
|
|
//! [`SourceMetadata`] is the whole of what can reach a file this crate writes.
|
|
//! It is populated field by field from whatever the caller decoded, and
|
|
//! nothing else travels — not because each unwanted tag is removed, but
|
|
//! because there is nowhere in this type for one to sit. That is the
|
|
//! difference between "we strip GPS" and "GPS cannot be written unless
|
|
//! [`SourceMetadata::location`] is `Some`", and only the second survives
|
|
//! somebody adding a field to the decoder next year.
|
|
//!
|
|
//! # What is deliberately not here
|
|
//!
|
|
//! **The maker note** (EXIF `0x927C`). It is an opaque vendor blob with no
|
|
//! public format, and its contents differ by body and firmware. Canon's
|
|
//! carries the body serial number and the shutter count; several bodies put a
|
|
//! *duplicate copy of the GPS fix* inside it, which is the specific reason it
|
|
//! cannot be passed through as an unexamined byte range: an export that
|
|
//! stripped the GPS directory and copied the maker note would have published
|
|
//! the coordinates anyway, while reporting itself as private. Parsing it per
|
|
//! vendor to decide what is safe is a research project with a permanent
|
|
//! maintenance cost, and the value on the other side is a few tags a
|
|
//! photographer rarely misses. So it is dropped, in both directions, whatever
|
|
//! the settings say.
|
|
//!
|
|
//! **Serial numbers and owner name** (`BodySerialNumber` 0xA431,
|
|
//! `LensSerialNumber` 0xA435, `CameraOwnerName` 0xA430). These identify a
|
|
//! person and a specific piece of equipment, and a serial number in a
|
|
//! published file links every photograph that person has ever posted. They
|
|
//! have no field here, so no export writes them.
|
|
//!
|
|
//! **IPTC and XMP.** FR-EXP-8 names both. Neither is read by `dr-decode`
|
|
//! today, so there is nothing to carry through; when there is, it arrives as
|
|
//! fields on this type and is written from them, and the same allowlist
|
|
//! reasoning applies unchanged.
|
|
|
|
use dr_types::Location;
|
|
|
|
/// TRACES: FR-EXP-8
|
|
/// The source metadata an export may carry.
|
|
///
|
|
/// Every field is optional because every field is genuinely absent from some
|
|
/// real file: scanner output has no aperture, a JPEG from a phone has no lens
|
|
/// model, and most photographs have no copyright statement at all.
|
|
///
|
|
/// Built by the caller, which is the only place that has both the decoded
|
|
/// source and the crate that decoded it — `dr-export` deliberately depends on
|
|
/// no decoder (see the crate docs), so the copy is made one field at a time
|
|
/// where both types are in scope. That transcription is a feature: it is the
|
|
/// point where somebody has to decide, in writing, that a newly-parsed piece
|
|
/// of the source is allowed to leave the machine.
|
|
#[derive(Debug, Clone, Default, PartialEq)]
|
|
pub struct SourceMetadata {
|
|
pub make: Option<String>,
|
|
pub model: Option<String>,
|
|
pub lens: Option<String>,
|
|
/// Exposure time in seconds.
|
|
pub shutter: Option<f32>,
|
|
/// The f-number, as in f/2.8.
|
|
pub aperture: Option<f32>,
|
|
pub iso: Option<u32>,
|
|
/// Millimetres, as marked on the lens rather than 35 mm equivalent.
|
|
pub focal_length: Option<f32>,
|
|
/// When the shutter fired, as Unix seconds read as a wall clock.
|
|
pub captured_at: Option<i64>,
|
|
/// Minutes east of UTC, where the camera recorded a zone.
|
|
pub captured_offset: Option<i32>,
|
|
/// Who made the photograph.
|
|
pub artist: Option<String>,
|
|
/// The rights statement.
|
|
pub copyright: Option<String>,
|
|
/// TRACES: FR-EXP-8
|
|
/// Where the shutter fired.
|
|
///
|
|
/// The one field the strip option is about. It is carried this far so that
|
|
/// a photographer who *wants* their coordinates can have them; by the time
|
|
/// the encoder sees the record this field has already been through
|
|
/// [`Self::sanitised`], and is `None` unless the user turned stripping
|
|
/// off.
|
|
pub location: Option<Location>,
|
|
}
|
|
|
|
impl SourceMetadata {
|
|
/// This record as the settings permit it to be written.
|
|
///
|
|
/// **The single place stripping happens.** The encoders below take a
|
|
/// record and write what is in it, with no view on privacy; concentrating
|
|
/// the decision here means there is one function to read to know what an
|
|
/// export can disclose, and no format can quietly disagree with the
|
|
/// others — the failure mode where JPEG honours the setting and TIFF, five
|
|
/// hundred lines away, does not.
|
|
///
|
|
/// Stripping empties the field rather than blanking it. A `GPSLatitude` of
|
|
/// `0/0` still announces that the camera had a fix and that this file has
|
|
/// been through a scrubber; an absent directory says nothing at all, and
|
|
/// says it in the same shape as the millions of files that never had one.
|
|
pub(crate) fn sanitised(&self, strip_location: bool) -> Self {
|
|
let mut out = self.clone();
|
|
if strip_location {
|
|
out.location = None;
|
|
}
|
|
out
|
|
}
|
|
}
|