summaryrefslogtreecommitdiff
path: root/crates/gipfel-cli/src/main.rs
diff options
context:
space:
mode:
authorJohannes Hofmann <Johannes.Hofmann@gmx.de>2026-10-26 20:05:20 +0100
committerJohannes Hofmann <Johannes.Hofmann@gmx.de>2026-10-26 20:05:20 +0100
commit375d87a9b0ec1cf2fffd705adb870c86144d2400 (patch)
tree1e8e05e7cd58686628d0f4622622456613a42e4d /crates/gipfel-cli/src/main.rs
parent45425dd047b791fe63f13a46a3251c529cfffc94 (diff)
Replace the C++ implementation with the Rust port
The Rust port (formerly gipfel-rs) is now the official successor to the FLTK/GSL C++ code base. This squashes the port into a single commit and removes the C++ sources (they remain in the git history below). Layout: crates/gipfel-core headless core: hills, panorama geometry, projections (maxima-generated derivatives), pure-Rust LM fit, gipfel: JPEG comment + EXIF I/O, profiles, stitching crates/gipfel-cli command-line tools (export, info, stitch, profiles) crates/gipfel-app Cosmic DE (libcosmic) GUI, binary name `gipfel` codegen/ maxima scripts + regen.sh for the projection code tests/ golden files + C++ reference harnesses (historical) The original README is kept for now; it will be updated to describe the Rust build. gipfel.dat, AUTHORS, ChangeLog and NEWS are unchanged.
Diffstat (limited to 'crates/gipfel-cli/src/main.rs')
-rw-r--r--crates/gipfel-cli/src/main.rs371
1 files changed, 371 insertions, 0 deletions
diff --git a/crates/gipfel-cli/src/main.rs b/crates/gipfel-cli/src/main.rs
new file mode 100644
index 0000000..f0690a8
--- /dev/null
+++ b/crates/gipfel-cli/src/main.rs
@@ -0,0 +1,371 @@
+//! gipfel-cli: headless command line tools, mirroring the C binary's
+//! `-p/--export-position`, `-e/--export-hills` and stitch modes.
+
+use gipfel_core::hill::FLAG_EXPORT;
+use gipfel_core::metadata::{apply_image_metadata, image_dimensions, ImageMetaData};
+use gipfel_core::panorama::Panorama;
+use gipfel_core::stitch::{self, ScanMode};
+
+fn usage() {
+ eprintln!(
+ "usage: gipfel-cli <command> [options]\n\
+ \n\
+ commands:\n\
+ \x20 export-position <img> print position CSV line\n\
+ \x20 export-hills <img> [-d <data>] [-e <hills>] export hill positions\n\
+ \x20 info <img> show parsed metadata\n\
+ \x20 stitch [-t out.tif [-4]] [-j out.jpg] [-w N]\n\
+ \x20 [-h N] [-r from,to] [-b] img... cylindrical panorama\n\
+ \x20 profiles list distortion profiles\n\
+ \x20 profile-save <name> <k0> <k1> <x0> save a profile\n"
+ );
+}
+
+fn export_position(img: &str) -> i32 {
+ // C reads metadata directly (no image decode); missing values print as nan
+ let mut md = ImageMetaData::new();
+ md.load_image(img);
+
+ // C: printf(",%s,,%f,%f,%d\n", img_file, latitude, longitude, (int)rint(height))
+ println!(
+ ",{},,{},{},{}",
+ img,
+ fmt_f(md.latitude),
+ fmt_f(md.longitude),
+ md.height.round_ties_even() as i64
+ );
+ 0
+}
+
+fn fmt_f(v: f64) -> String {
+ if v.is_nan() {
+ "nan".to_string()
+ } else {
+ format!("{:.6}", v)
+ }
+}
+
+fn export_hills(img: &str, data: &str, export_file: Option<&str>) -> i32 {
+ let Some((w, h)) = image_dimensions(img) else {
+ eprintln!("could not read image {}", img);
+ return 1;
+ };
+
+ let mut md = ImageMetaData::new();
+ md.load_image(img);
+
+ let mut pan = Panorama::new();
+ let have_info = apply_image_metadata(&mut pan, &md, w, h);
+
+ if let Err(e) = pan.load_data(data) {
+ eprintln!("Could not load datafile {}: {}", data, e);
+ return 1;
+ }
+ pan.set_height_dist_ratio(0.07); // C default "visibility"
+
+ if !have_info {
+ eprintln!("No gipfel info available for {}.", img);
+ return 0;
+ }
+
+ // -e: restrict export to the hills from the given file
+ if let Some(file) = export_file {
+ match gipfel_core::hill::load(file) {
+ Ok(mut extra) => {
+ for m in extra.iter_mut() {
+ m.flags |= FLAG_EXPORT;
+ }
+ pan.add_hills(&extra);
+ }
+ Err(_) => return 1,
+ }
+ }
+
+ println!("#\n# name\theight\tx\ty\tdistance\tflags\n#");
+
+ for row in pan.export_hill_rows(w as i64, h as i64, export_file.is_some()) {
+ println!("{}", row);
+ }
+
+ if export_file.is_some() {
+ for m in pan.hills.iter_mut() {
+ m.flags &= !FLAG_EXPORT;
+ }
+ }
+
+ 0
+}
+
+/// Port of the C stitch mode: resample the given images into one
+/// cylindrical panorama. Flags follow the C binary: -w/-h output size,
+/// -r from,to azimuth range in degrees, -b bicubic, -4 16-bit TIFF,
+/// -j/-t output file (tiff wins if both given, as in C).
+fn stitch(imgs: &[String], opts: &StitchOpts) -> i32 {
+ if imgs.is_empty() || (opts.tiff.is_none() && opts.jpeg.is_none()) {
+ usage();
+ return 2;
+ }
+ let mut sources = Vec::new();
+ for img in imgs {
+ match stitch::StitchSource::load(img) {
+ Ok(s) => sources.push(s),
+ Err(e) => {
+ eprintln!("{e}");
+ return 1;
+ }
+ }
+ }
+ let mode = if opts.bicubic { ScanMode::Bicubic } else { ScanMode::Nearest };
+ let out = stitch::resample(&sources, mode, opts.w, opts.h, opts.from, opts.to);
+ let hit = out.hit.iter().filter(|h| **h).count();
+ eprintln!(
+ "stitched {}x{}: {}/{} pixels covered",
+ out.w, out.h, hit, out.hit.len()
+ );
+ let res = if let Some(path) = &opts.tiff {
+ stitch::write_tiff(path, &out, opts.bits16)
+ } else {
+ stitch::write_jpeg(opts.jpeg.as_ref().unwrap(), &out, 90)
+ };
+ match res {
+ Ok(()) => 0,
+ Err(e) => {
+ eprintln!("{e}");
+ 1
+ }
+ }
+}
+
+struct StitchOpts {
+ w: u32,
+ h: u32,
+ from: f64,
+ to: f64,
+ bicubic: bool,
+ bits16: bool,
+ tiff: Option<String>,
+ jpeg: Option<String>,
+}
+
+/// C defaults: stitch_w=2000, stitch_h=500, stitch_from=0.0, stitch_to=380.0
+fn stitch_defaults() -> StitchOpts {
+ StitchOpts {
+ w: 2000,
+ h: 500,
+ from: 0.0,
+ to: 380.0,
+ bicubic: false,
+ bits16: false,
+ tiff: None,
+ jpeg: None,
+ }
+}
+
+fn stitch_cmd(args: &[String]) -> i32 {
+ let mut o = stitch_defaults();
+ let mut imgs = Vec::new();
+ let mut i = 0;
+ while i < args.len() {
+ let a = args[i].as_str();
+ let need = |i: &mut usize| -> Option<String> {
+ if *i + 1 < args.len() {
+ *i += 1;
+ Some(args[*i].clone())
+ } else {
+ None
+ }
+ };
+ let ok = match a {
+ "-w" => match need(&mut i) {
+ Some(v) => {
+ o.w = v.parse().unwrap_or(2000);
+ true
+ }
+ None => false,
+ },
+ "-h" => match need(&mut i) {
+ Some(v) => {
+ o.h = v.parse().unwrap_or(500);
+ true
+ }
+ None => false,
+ },
+ "-r" => match need(&mut i) {
+ Some(v) => {
+ // C: atof(optarg) [, atof(after comma)] — atof reads the
+ // numeric prefix before the comma
+ let (first, second) = v.split_once(',').unwrap_or((v.as_str(), ""));
+ o.from = first.trim().parse().unwrap_or(0.0);
+ if !second.trim().is_empty() {
+ o.to = second.trim().parse().unwrap_or(380.0);
+ }
+ true
+ }
+ None => false,
+ },
+ "-j" => match need(&mut i) {
+ Some(v) => {
+ o.jpeg = Some(v);
+ true
+ }
+ None => false,
+ },
+ "-t" => match need(&mut i) {
+ Some(v) => {
+ o.tiff = Some(v);
+ true
+ }
+ None => false,
+ },
+ "-b" => {
+ o.bicubic = true;
+ true
+ }
+ "-4" => {
+ o.bits16 = true;
+ true
+ }
+ _ => {
+ if a.starts_with('-') {
+ false
+ } else {
+ imgs.push(a.to_string());
+ true
+ }
+ }
+ };
+ if !ok {
+ usage();
+ return 2;
+ }
+ i += 1;
+ }
+ stitch(&imgs, &o)
+}
+
+fn info(img: &str) -> i32 {
+ let mut md = ImageMetaData::new();
+ md.load_image(img);
+ println!("manufacturer: {:?}", md.manufacturer);
+ println!("model: {:?}", md.model);
+ println!("longitude: {}", md.longitude);
+ println!("latitude: {}", md.latitude);
+ println!("height: {}", md.height);
+ println!("direction: {}", md.direction);
+ println!("pitch: {}", md.pitch);
+ println!("roll: {}", md.roll);
+ println!("focal_length: {}", md.focal_length);
+ println!("focal_length_35mm: {}", md.focal_length_35mm);
+ println!("projection type: {}", md.projection_type);
+ println!("k0: {}", md.k0);
+ println!("k1: {}", md.k1);
+ println!("x0: {}", md.x0);
+ 0
+}
+
+fn profiles() -> i32 {
+ let all = gipfel_core::profile::list();
+ if all.is_empty() {
+ eprintln!("no profiles in {}", gipfel_core::profile::prefs_path().display());
+ }
+ for (name, p) in all {
+ println!("{name}\tk0={}\tk1={}\tx0={}", p.k0, p.k1, p.x0);
+ }
+ 0
+}
+
+fn profile_save(name: &str, k0: f64, k1: f64, x0: f64) -> i32 {
+ let p = gipfel_core::profile::Profile { k0, k1, x0 };
+ match gipfel_core::profile::save(name, p, true) {
+ Ok(()) => {
+ eprintln!("saved profile {name}");
+ 0
+ }
+ Err(e) => {
+ eprintln!("{e}");
+ 1
+ }
+ }
+}
+
+fn profile_save_cmd(args: &[String]) -> i32 {
+ if args.len() != 4 {
+ usage();
+ return 2;
+ }
+ let (Ok(k0), Ok(k1), Ok(x0)) = (
+ args[1].parse::<f64>(),
+ args[2].parse::<f64>(),
+ args[3].parse::<f64>(),
+ ) else {
+ usage();
+ return 2;
+ };
+ profile_save(&args[0], k0, k1, x0)
+}
+
+fn main() {
+ let args: Vec<String> = std::env::args().collect();
+ let Some(cmd) = args.get(1) else {
+ usage();
+ std::process::exit(2);
+ };
+
+ let code = match cmd.as_str() {
+ "export-position" => {
+ if args.len() != 3 {
+ usage();
+ 2
+ } else {
+ export_position(&args[2])
+ }
+ }
+ "export-hills" => {
+ if args.len() < 3 {
+ usage();
+ 2
+ } else {
+ let img = &args[2];
+ let mut data = "gipfel.dat".to_string();
+ let mut export_file: Option<String> = None;
+ let mut i = 3;
+ while i < args.len() {
+ match args[i].as_str() {
+ "-d" => {
+ if i + 1 < args.len() {
+ data = args[i + 1].clone();
+ i += 1;
+ }
+ }
+ "-e" => {
+ if i + 1 < args.len() {
+ export_file = Some(args[i + 1].clone());
+ i += 1;
+ }
+ }
+ _ => {}
+ }
+ i += 1;
+ }
+ export_hills(img, &data, export_file.as_deref())
+ }
+ }
+ "info" => {
+ if args.len() != 3 {
+ usage();
+ 2
+ } else {
+ info(&args[2])
+ }
+ }
+ "profiles" => profiles(),
+ "profile-save" => profile_save_cmd(&args[2..]),
+ "stitch" => stitch_cmd(&args[2..]),
+ _ => {
+ usage();
+ 2
+ }
+ };
+
+ std::process::exit(code);
+}