# Photo

The optional camera-aim photo saved with a logged group: a reference to the picture, the zoom used and the aim reading it was taken with, and who can see the picture.

## Generated table

**Record:** observation · **Covers:** `photo`

| Field | Type | Required | Limits in the schema | Solver reads | In a shared log |
|---|---|---|---|---|---|
| `photo.aim` | object | yes, within | - | no | removed when location is hidden or photos are off |
| `photo.aim.az_mag_deg` | number | yes, within | - | no | removed for every reader |
| `photo.aim.az_sd` | number | yes, within | min 0 | no | removed when location is hidden or photos are off |
| `photo.aim.az_true_deg` | number or null | yes, within | - | no | removed when location is hidden or photos are off |
| `photo.aim.incl_deg` | number | yes, within | - | no | removed when location is hidden or photos are off |
| `photo.aim.incl_sd` | number | yes, within | min 0 | no | removed when location is hidden or photos are off |
| `photo.asset` | string | yes, within | pattern ^asset:[0-9a-f]{64}$ | no | removed when location is hidden or photos are off |
| `photo.taken_at` | string | yes, within | - | no | removed when location is hidden or photos are off |
| `photo.zoom` | number | yes, within | min 1; max 20 | no | removed when location is hidden or photos are off |
| `photo.zoom_kind` | `'hardware'` or `'digital'` | yes, within | - | no | removed when location is hidden or photos are off |

## What it holds

A group can carry one photo, taken through the camera aim sheet. The group holds a reference to the picture and the details of the shot that took it: when, at what zoom, and where the phone was pointing. The picture itself is not in the record. An absent `photo` means no photo was kept, which is the usual case. A photo of the target that holes are marked on is a different picture, kept in [target marks](marks.md).

## What to know first

**A shared log shows its photos only when it shows the location and its Photos switch is on.** Otherwise a reader gets no photo and no picture. With location shown the photo comes without its magnetic bearing.

**The record holds a reference, not the picture.** The picture lives in the account's image store. A backup file carries the reference only.

**The connector sees the details, never the picture.** `get_session` returns `photo` as stored; no tool can read, attach or delete a picture.

**Deleting a group leaves its picture stored.** Only "Delete photo" in the full-screen viewer removes the picture and frees the space.

**Zoom never changes the aim.** The angles come from the phone's orientation, not the picture.

## The fields

**`photo.asset`** The reference to the picture, written `asset:` and then 64 lower-case hexadecimal characters, the SHA-256 of the picture's bytes. It is never a web address. The same reference form is used for theme backgrounds. Two groups name the same picture only if the bytes are identical. Required inside `photo`. Example: asset: followed by 64 characters of a made-up hash.

**`photo.taken_at`** The time the Capture button was pressed, as an ISO date and time in UTC, to the millisecond. It is the time of the frame, not of the shot and not of the log: the group's own `created_at` is later, by the time spent averaging and filling in the form. Example: 2026-10-14T15:18:09.221Z.

**`photo.zoom`** The camera zoom when the frame was taken, 1 for none. With a camera that has real zoom, it is the camera's level (as high as 20); otherwise the app crops the picture from its centre and the level is 1 to 4. The zoom control rounds it to two decimals. Example: 2.5.

**`photo.zoom_kind`** `hardware` when the camera zoomed, `digital` when the app cropped the frame. It tells you whether the picture lost pixels to the crop.

**`photo.aim`** The aim reading the photo was taken with: five members, described next. All five are required whenever a photo is present.

**`photo.aim.az_true_deg`** and **`photo.aim.az_mag_deg`** The compass bearing the phone pointed at, in degrees, averaged over about one second of readings. The first is from true north. It is null when the app had no declination (no position fix) or the phone gave no compass north; in the second case the other value is a heading from an arbitrary start, not a compass bearing. The second is from magnetic north when the phone gave one, and is always present. The app stores what the aim reading gave (0 to 360); the schema does not enforce that range.

**`photo.aim.incl_deg`** The slope the phone pointed at, in degrees, positive upward. It is the same kind of number as the group's inclination.

**`photo.aim.az_sd`** and **`photo.aim.incl_sd`** How much the bearing and the slope readings varied during the averaging, in degrees (a standard deviation). Never below 0; a bad value is stored as 0. A large value means the phone moved while it was read.

**What the picture is.** A JPEG made from the camera frame at the moment of Capture: cropped for digital zoom, no more than 2,000 pixels on the long side, quality 0.85, re-drawn through a canvas so the camera's own metadata (including location) does not reach it. Before the picture is named, the app also removes any metadata block the browser's own encoder wrote, keeping the same blocks the server allows. The server refuses a photo that still carries place or camera details ("This photo still carries place or camera details. Update the app and try again."), and never hands a stored one that carries them to a reader. It has no crosshair and no caption. The crosshair, the range, the bearing, the slope and the time are drawn only on the copy the viewer's save icon sends to the phone, with the copy's name `cold-bore-lab-date-rangeyd.jpg`; that copy is not stored. The crop, the 2,000 pixel limit and the JPEG settings were run against a stand-in canvas; that a real camera frame comes out with no metadata is read from the code, not run.

**Limits, by writer.** There is no profile-style table: the numbers are not checked by the profile save, and the connector writes none of them.

- App: `sanePhoto` checks a draft's photo (restored, or handed over from the Calculator) and checks it again at save. It drops the photo if the reference or the zoom kind is wrong, the time cannot be read as a date, or the zoom or a bearing or slope is not a number. It clamps the zoom to 1 to 20, turns a negative or missing spread into 0, and turns a missing true bearing into null. A dropped photo never blocks the group; the form says "Photo not saved."
- Stored record, checked when the group syncs (run for this page): the limits are in the table above. Not checked: a bearing of 400, a magnetic bearing of -50 and a slope of 123 are accepted, and so is any text as the time, the empty text included. An unknown key inside `photo`, a missing member, or a `photo` of null refuses the whole group.
- Connector: `log_group` cannot attach a photo, and the connector can neither read nor send a picture.
- Size: a picture is at most 10 MB when uploaded, and an account's photos at most 1 GB together.

## Who writes it

**The app.** The camera aim sheet has a "Save photo" switch, off every time the sheet opens. With it on, Capture takes the frame before averaging the aim, and Use hands both back. On the Session screen the photo becomes the draft of the group being typed, and the group is saved with it. On the Calculator it waits as the pending photo, kept across a reload for the account that took it, and "Log this shot" carries it into the new group's draft and clears it from the Calculator. A second photo for the same group replaces the first; the first picture stays stored. "Remove the photo" on a draft only forgets it. If the picture cannot be encoded within three seconds, the aim is still used and the form says "Photo not saved."

**Where the picture goes.** At capture the picture is stored on the device only. It goes to the account when a saved group names it: at once when signed in and online, otherwise on the next sync. A photo the account has no room for stays on the device, and its thumbnail says "Photo not saved to your account: photo space is full". On another device the thumbnail reads "photo unavailable" until the picture has uploaded.

**Delete photo.** In the full-screen viewer of your own group, after a question in the page, the app saves the group without `photo` (its `updated_at` becomes now, every other field stays), drops the device copy unless another live group, a theme or the Calculator's pending photo uses the same picture, and, when signed in, queues the server delete for the next sync (signed out, nothing is queued and the account's copy stays). The server refuses to delete while any live group still names the picture, so the device tries again after the group's change has synced.

**Not the connector, the fitter or the server.** None of them writes this field. A backup restore writes whatever the file held; it does not check the photo, and the picture is not in the file.

## Who reads it

Nothing in the solver or the fitter reads a photo. A group with a photo fits exactly like one without.

**The app.** The Session screen shows a 56 pixel thumbnail under each group that has a photo; a tap opens the full-screen viewer (pinch to 6 times, drag to pan, the crosshair and a caption drawn on top, a save icon for the stamped copy). Since 2026-10-24, a photo with no true bearing says so under the caption: the phone gave no compass north or had no position fix, so the direction it pointed is not known (owner ruling; the stored magnetic value is then not a compass bearing). Your own pictures come from the device, or from the account's image store when the device lacks them. A shared log's pictures come from the share's photo route and are held only as a temporary link, never kept on the device. The viewer of a shared log has no delete icon.

**The connector.** `get_session` returns each group as stored, so it includes the `photo` details. It returns no picture, and there is no connector tool that reads one: the image list the connector can see holds only theme backgrounds.

**Exports.** The JSON backup holds the `photo` details; the CSV export has no photo column. The backup's count of images not included counts theme backgrounds only, so it does not mention photos.

**The server.** A shared log's view that shows the location and the photos carries the details, without the magnetic bearing, and the share's photo route serves the picture. The admins' pooled data carries the details in its JSON view but not in its CSV.

## Why it is stored

A shooter who aims through the camera has the target in the frame, and the picture is a record of what the target and the surroundings looked like when the group was shot. The bearing, the slope and the time say which way and when. The zoom says how to read the picture. The picture is kept apart from the record so that sync stays small and a picture that has not uploaded yet never holds up a group. The spreads say how steady the reading was.

## Changing it

The photo is part of the group's record, which is changed in place and not versioned. The only changes are a replacement before the group is saved, and Delete photo afterwards. There is no way to attach a photo to a saved group, and none to replace it. Deleting the whole group (a soft delete) does not delete the picture: it stays in the account's image store and counts against the 1 GB until it is removed some other way. Delete photo is the only action that frees the space, and only if no other live group or theme background uses the same picture. A saved calibration does not depend on a photo, but removing one changes the group's `updated_at`, so a connector save made from a preview made before the removal is refused (the preview's fingerprint covers `updated_at`).

## Sharing and privacy

A shared log carries a group's `photo` only when the share shows the location **and** its **Photos** switch is on. The Photos switch is off by default for a public share, and on for a share with one person or with the admins; the sharer can turn it either way on any share. Shares made before the switch existed were set the same way (owner decision 2026-10-15), so photos already shared publicly stopped showing until the sharer turned the switch on. A public share also hides the location by default. With both on, the reader gets `photo.asset`, `photo.taken_at`, `photo.zoom`, `photo.zoom_kind` and the `photo.aim` members as stored, except `photo.aim.az_mag_deg`, the compass reading before the declination was applied: it is removed for every reader, because the declination gives the place back and the true bearing is enough. With either off, the whole `photo` is removed and the picture's route answers as for an unknown share. This holds for every audience, whatever the notes switch says. The share sheet shows the Photos switch under the location switch, and says "Photos are shared only with location shown, and can show where you were." while it is on. The Terms and privacy page says how both switches work. (Until 2026-10-15 a photo was shared whatever the location switch said; the owner's decisions of that date changed it.)

With location shown and the Photos switch on:

| Reader | Sees the details | Sees the picture |
|---|---|---|
| The person a log is shared with (signed in with Google as that email) | yes | yes |
| An admin, on a share with the admins or a public share | yes | yes |
| Anyone with the link of a public share | yes | yes |
| Anyone, on a public share listed on Community | yes | yes |
| Someone who has not opened it by its link, on a public share not listed on Community, asking by its id | no | no |
| Anyone, on a stopped or unknown share | no | no |

A reader gets a picture only through the share, and only while all of these hold: the share shows the location, its Photos switch is on, the share is live, the profile still has a live version, the reader may open the share, a live group of the shared profile in a live session names that picture, and the sharer's device has uploaded the picture, and the picture carries no image metadata. Anything else gets the same "not shared" answer (404) as a stopped share or a wrong code, with a no-store cache rule, so the answer does not say which case it was. A reader who saved the stamped copy, or a screenshot, keeps it after the share stops.

What a reader sees in the picture itself is the sharer's choice of frame. The details give the true compass bearing, the slope and the time.

The `use` permission on a share (anything, pooled results only, helping me only) says what admins agree to do with the log. The photo route does not look at it.

---

[All reference pages](../index.md)
