- Vue 67%
- Rust 32.5%
- TypeScript 0.5%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .forgejo/workflows | ||
| .idea | ||
| data | ||
| src | ||
| template | ||
| tests | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| License | ||
| README.md | ||
Subsurface Dive Log Generator
Subsurface is my favourite dive log app. It supports almost every dive computer I am likely to use and its open source.
That said, the HTML export functionality is not really the prettiest, and sometimes I would like to share the details of a dive with other people that I dived with.
Enter this Rust CLI tool, point it at the XML file that Subsurface stores your log information in. It will then generate a Nuxt-4-based website of your dive log, which you can then upload to a web server so that others can see it.
My own dive log site generated with the tool can be found here if you want to see how it looks.
Features
- Parses the Subsurface XML dive log format (version 3)
- Generates a fully static, self-contained Nuxt 4 website consisting of two-page layouts:
- Dive listing page — searchable grid of all dives showing site name, location, depth, duration, water temperature, and tags
- Dive detail page — full statistics, star rating, visibility, SAC rate, air consumed, buddy, divemaster, notes, an interactive map (OpenStreetMap via Leaflet), a depth/temperature profile chart rendered as SVG, and a photo carousel with thumbnail strip and full-screen lightbox
- Optional reverse geocoding of dive site location information via Nominatim to resolve GPS coordinates to human-readable city/region names
- Optional image processing — resizes and optimises dive photos for the web, with a cache to avoid reprocessing unchanged files
Requirements
Building
git clone <repo>
cd dive_log
cargo build --release
The compiled binary is at target/release/dive_log.
Usage
dive_log --file <path/to/divelog.xml> [OPTIONS]
Options
| Flag | Short | Default | Description |
|---|---|---|---|
--file <FILE> |
-f |
(required) | Path to the Subsurface XML dive log file |
--output <DIR> |
-o |
output |
Directory to write the generated Nuxt 4 site into |
--geocode |
disabled | Reverse-geocode dive sites that have GPS coordinates but no region/city data (see below). Results are cached in data/geocache.json. |
|
--template <DIR> |
./template/ if present |
Directory of customised Nuxt template files (see Customising the generated site) | |
--media <DIR> |
<log_dir>/media/ if present |
Directory containing dive images, organised into sub-folders by dive number (see Adding photos to dives). | |
--max-image-width <PIXELS> |
2000 |
Maximum width in pixels for processed dive images. Images wider than this are scaled down proportionally; narrower images are left at their original size. | |
--help |
-h |
Print help |
Basic usage example
dive_log --file ~/Documents/divelog.xml
Parses the log and writes the site to the ./output/ directory.
Custom output directory
dive_log --file ~/Documents/divelog.xml --output ~/Sites/my-dive-log
Dive Location Geocoding
Geocoding is available to look up region names for dive sites that have GPS information associated with them.
To ensure fast site generation, geocoding is disabled by default, and can be enabled with the --geocode flag e.g.
dive_log --file ~/Documents/divelog.xml --geocode
The geocoding feature uses the Nominatim API (OpenStreetMap). Nominatim's usage policy requires at most one request per second, so this adds roughly one second per site that needs geocoding.
Since this is a free service, results are cached in data/geocache.json file to avoid excessive API calls.
Geocoding cache
Results are persisted to data/geocache.json next to your dive log file. On later runs, any site whose GPS coordinates are already in the cache is resolved instantly without hitting the API — including sites where no locality was found (open water). The progress line reports how many lookups come from cache vs. API, so you can see at a glance how much work remains.
The cache file is a plain JSON object keyed by "lat,lon" strings and is safe to commit to version control so collaborators share the same cached results. But by default this file is excluded from commits via the .gitignore file.
It is not written to the generated site.
Adding photos to dives
You can attach photos to any dive by placing them in a folder named after the dive number inside a media directory.
When running the tool, it will resize the images (can be set using the flag --max-image-width). By default, will resize to 2000px wide and optimise them for the web.
By default, dive_log looks for a media/ in the data/ directory and then searches for numbered subfolders within.
To add images to a logged dive, find the dive number in the subsurface, create a folder with the same name as the dive number and place the images in it.
After that you can run the tool again, and the images will be processed and added to the dive detail page.
It is possible to use a different folder for containing the dive media by passing directory path with the --media. But the numbered subfolder structure is still required to match the images to the relevant dive at run time.
dive_log --file data/divelog.xml --media /Volumes/Photos/dive-media
Folder structure
Inside the media directory, create one subfolder per dive named after its dive number (the number shown in Subsurface's dive list, not an internal ID):
media/
├── 42/ # photos for dive #42
│ ├── IMG_0001.JPG
│ └── IMG_0002.JPG
└── 43/ # photos for dive #43
└── DSC_0123.jpg
Supported formats
JPEG (.jpg, .jpeg) and PNG (.png) files are processed when found in a numbered folder that matches a logged dive.
File name matching is case-insensitive, so .JPG, .JPEG, and .PNG are all recognised.
What the tool does
When dive_log runs and finds a media/ directory next to the log file, it automatically processes every image folder it finds:
- Loads each source image using its original pixel data.
- Resizes — if the image is wider than the maximum width (default 2 000 px), it is scaled down proportionally using a Lanczos3 (high-quality) filter. Images already at or below the maximum width are left at their original size. Control this with
--max-image-width. - Converts the image to RGB colour space (strips any alpha channel) and re-encodes it as a JPEG at quality 85 — a good balance between visual fidelity and file size.
- Writes the processed image to
output/public/media/<dive_number>/<filename>.jpgso it is served as a static asset alongside the rest of the site. - Updates the dive's metadata to record which image files belong to it, so the generated dive detail page knows what to display.
Output filenames always use the .jpg extension regardless of the source format.
Caching
Processing large batches of images can be slow. To avoid reprocessing files that have not changed, dive_log maintains a cache file at data/mediacache.json (next to the log file). Each entry records a fingerprint — the file size and modification time — for a source image. On subsequent runs:
- If the fingerprint matches and the output file already exists, the image is skipped instantly.
- If the source file has been replaced or modified (different size or mtime), it is reprocessed and the cache is updated.
The cache file is written only when at least one image is actually processed. It is a local scratch file and is excluded from version control via .gitignore.
Serving the generated site
After generation, install the dependencies with npm and start the development server:
cd output # or whatever --output path you used
npm install
npm run dev # live development server at http://localhost:3000
To build a fully static site for deployment to your web server:
npm run generate # outputs to output/.output/public/
npm run preview # preview the static build locally
The generate command pre-renders every dive page as static HTML and bundles all assets, so the result can be hosted on any static file host (GitHub Pages, Netlify, S3, etc.) with no server-side runtime required.
To automate this process, I have personally just used a bash script to generate the site and then upload to my webserver using rsync.
Generated site structure
output/
├── nuxt.config.ts
├── package.json
├── tsconfig.json
├── app/
│ ├── app.vue # Root layout (header, footer, global styles)
│ ├── pages/
│ │ ├── index.vue # Dive listing with search
│ │ └── dives/[id].vue # Individual dive detail page
│ ├── components/
│ │ ├── DiveMap.vue # Leaflet map (client-side only)
│ │ └── DiveProfile.vue # SVG depth + temperature profile chart
│ └── assets/data/
│ ├── dives.json # All dive metadata (imported at build time)
│ └── sites.json # All dive site data including GPS and geo info
└── public/
└── data/
│ └── profiles/
│ └── <id>.json # Per-dive depth/temperature profile points
└── media/ # Processed dive photos (only present when media/ source exists)
└── <dive_number>/
└── <filename>.jpg # Resized and optimised JPEG
Dive information Layout
Dive information is presented on the generated Vue-based site via two main page layouts:
Dive Listing page
Lists all the dives that are in your dive log presenting:
- Dive number and date
- Dive site name
- Location (city/region/country when available)
- Maximum depth
- Duration
- Water temperature
- SAC rate (if recorded)
- Tags
The search box filters across site name, location, date, buddy name, and tags.
Dive Detail page
The dive detail page is split into the following submodules:
Header — always shown
| Field | Source |
|---|---|
| Date & time | Dive log |
| Rating | ★ 1–5 stars from Subsurface |
| Visibility | ★ 1–5 stars from Subsurface (shown alongside rating) |
Dive Statistics panel
| Field | Source |
|---|---|
| Duration | Dive log |
| Max / mean depth | Dive computer |
| Water temperature | Dive computer |
| Buddy | Dive log |
| Divemaster | Dive log |
| OTU / CNS | Dive computer (oxygen tracking) |
| Tags | Dive log |
| Equipment | Cylinder size, O₂%, start/end pressures |
Gas Information panel — shown when cylinder data is present
Displays a per-tank breakdown for each cylinder:
| Field | Source |
|---|---|
| Gas | Mix type (Air or EANx%) |
| Start / end gas | Cylinder pressures from dive computer samples |
| Gas consumed (per tank) | Calculated from cylinder pressures × tank volume (L) |
| SAC rate (per tank) | Calculated from gas consumed ÷ duration ÷ ambient pressure |
Computer Information panel — shown when a dive computer model is recorded
| Field | Source |
|---|---|
| Computer | Model name from dive computer |
| Extra data | All <extradata> fields logged by the computer (firmware version, serial number, deco model, battery, etc.) |
Location panel
| Field | Source |
|---|---|
| Map | Interactive Leaflet map (OpenStreetMap) when GPS is available |
| Coordinates | GPS from dive site |
| Country | From dive site or resolved via geocoding |
Notes, Photos, and Dive Profile panels appear below the two-column layout when the relevant data is present. The profile is an SVG chart of depth over time with a filled gradient, max-depth marker, and a temperature strip when sample data is recorded. The Photos panel shows a carousel with thumbnail strip and full-screen lightbox — see Adding photos to dives.
Customising the generated site
The template/ directory in this repository contains the base Nuxt 4 source files used to build the generated site. You can edit any of these files to change the site's look, layout, or behaviour — then simply re-run dive_log and your changes are used instead of the built-in defaults.
Template directory structure
template/
├── package.json # Node dependencies (nuxt, leaflet)
├── nuxt.config.ts # Nuxt configuration
├── tsconfig.json
├── public/
│ └── robots.txt # Crawler directives (served at /robots.txt)
└── app/
├── app.vue # Root layout — header, footer, global CSS variables
├── pages/
│ ├── index.vue # Dive listing with search, stats bar
│ ├── dives/[id].vue # Dive detail page
│ ├── tags/[tag].vue # Filtered listing by tag
│ └── countries/[country].vue # Filtered listing by country
└── components/
├── DiveMap.vue # Leaflet map (client-side only)
└── DiveProfile.vue # SVG depth + temperature profile chart
Common customisations
| What to change | File to edit |
|---|---|
| Colour scheme, fonts, global layout | app/app.vue — edit the :root CSS variables |
| Site title and branding | app/app.vue — edit the <header> block |
| Dive card layout and which stats are shown | app/pages/index.vue |
| Dive detail statistics order or labels | app/pages/dives/[id].vue |
| Map style or zoom level | app/components/DiveMap.vue |
| Profile chart colours, size, or grid | app/components/DiveProfile.vue |
| Search engine and crawler directives | public/robots.txt |
Note: The
app/assets/data/andpublic/data/directories inside the output are always regenerated from your dive log — do not put custom files there.
Credits & Thanks
Thanks are due to Subsurface it's a fantastic project, and it makes downloading dives from your computer easy.
Licence
Copyright © 2026 Anthony Mills
This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.