use anyhow::{Context, Result, bail};
use thermark::config::Config;
use thermark::geometry::LabelMm;
use thermark::printer::PrintOptions;
use tracing::info;
use crate::cli::args::{CalibrateCommand, PrintCommand};
use crate::cli::session::{print_file_resolved, render_and_print_gray_resolved, resolve_target};
use crate::cli::tips::warn_print_limits;
pub async fn print(cfg: &Config, args: PrintCommand) -> Result<()> {
let PrintCommand {
conn,
task,
image,
model,
density,
rotate,
threshold,
fit,
label,
fill,
no_fill,
margin,
dither,
no_trim,
full_bleed,
preview,
} = args;
if !image.exists() {
bail!(
"image not found: {}\n \
tip: personal prints live under local/prints/ (gitignored), not fixtures/",
image.display()
);
}
warn_print_limits(&image, &label, no_fill, dither);
let target = resolve_target(cfg, model, &task)?;
let profile = thermark::profile_for_model(target.model);
let label_mm = label.as_deref().map(LabelMm::parse).transpose()?;
let use_fill = !no_fill && label_mm.is_some() && fill;
let opts = PrintOptions {
density,
rotate,
threshold,
fit,
label: label_mm,
fill: use_fill,
margin_px: margin,
dither,
safe: if full_bleed {
thermark::geometry::SafeArea::NONE
} else {
cfg.resolve_safe_area(profile.pixels_per_mm())
},
trim: !no_trim,
};
if let Some(out) = preview {
let composed = thermark::printer::compose_for_label(
&image,
&opts,
profile.max_width_px,
profile.pixels_per_mm(),
)?;
let gray = thermark::image_encode::grayscale_on_white(composed);
let visible = thermark::image_encode::render_print_preview(
&gray,
profile.max_width_px,
opts.threshold.get(),
opts.dither,
)?;
visible
.save(&out)
.with_context(|| format!("save {}", out.display()))?;
println!("preview written to {} (nothing printed)", out.display());
return Ok(());
}
print_file_resolved(cfg, &conn, target, &image, opts, full_bleed).await?;
println!("OK — sent print job");
Ok(())
}
fn print_calibration_legend(
lp: thermark::geometry::LabelPx,
safe: thermark::geometry::SafeArea,
pixels_per_mm: f64,
) {
use thermark::image_encode::CALIBRATION_RINGS;
let step_mm = (0.5 * pixels_per_mm).round().max(1.0) / pixels_per_mm;
println!();
println!("How to read it:");
println!(
" THICK box = the configured safe area (top {} / bottom {} / left {} / right {} px).",
safe.top, safe.bottom, safe.left, safe.right
);
println!(" If it printed complete on all 4 sides, you are good.");
println!(
" THIN rings = {CALIBRATION_RINGS} rings, {step_mm} mm apart, outermost at the very edge."
);
println!(" Count how many are complete on each edge:");
println!(" ring N complete -> that edge needs {step_mm} mm x N of margin.");
println!(" SIDE ticks = a feed ruler from the top edge: short = 1 mm,");
println!(" long = 5 mm. Read off the LAST tick that printed —");
println!(" that is the observed reach for this charged run.");
println!(" If repeated runs disagree, charge the printer before");
println!(" treating the result as a registration margin.");
println!(" Diagonals = skew check; the X should meet exactly at the centre cross.");
println!();
println!("Some side-to-side white border can be physical, not a bug:");
println!(" Across: media wider than the profile's printhead is clamped.");
println!(" If it is lopsided, re-centre the roll with the guide.");
println!(" Feed: the default 1 mm top/bottom inset is registration");
println!(" insurance; use the boundary probe before changing it.");
println!();
println!(
"Canvas {}x{} px. Re-run any time:",
lp.width_px, lp.height_px
);
println!(" thermark calibrate --label 50x30");
}
fn print_boundary_legend(label: thermark::geometry::LabelPx, pixels_per_mm: f64) {
let range = thermark::label::boundary_range(label, pixels_per_mm);
let (from, to) = (range.start(), range.end());
println!();
println!("One numbered bar per millimetre, {from}..{to} mm from the top.");
println!("Each sits at its own horizontal position, so nothing crowds.");
println!();
println!(" Read the HIGHEST number whose bar printed completely.");
println!(" That is where this printer stops on this media.");
println!();
println!(" If the LAST bar printed, the printer reaches the whole label —");
println!(" there is no unprintable band, and any inset is only");
println!(" registration margin. Charge first: a low battery truncates");
println!(" dense pages and reads as a printable-area limit.");
println!();
println!("Then save it — the tool works out the inset:");
println!(" thermark config safe-area --last-tick <that number> --label 50x30");
println!();
println!("Registration varies slightly between labels, so if two runs differ,");
println!("use the LOWER number.");
}
pub async fn calibrate(cfg: &Config, args: CalibrateCommand) -> Result<()> {
let CalibrateCommand {
conn,
task,
model,
label,
density,
boundary,
} = args;
let target = resolve_target(cfg, model, &task)?;
let label = cfg.resolve_label(label.as_deref());
let label_mm = LabelMm::parse(&label)?;
let (lp, safe, pixels_per_mm) =
render_and_print_gray_resolved(cfg, &conn, target, density, |detected| {
let pixels_per_mm = detected.pixels_per_mm();
let lp = label_mm.to_pixels(detected.max_width_px, pixels_per_mm);
let safe = cfg.resolve_safe_area(pixels_per_mm);
info!(
model = %detected.model,
dpi = detected.dpi,
width_px = lp.width_px,
height_px = lp.height_px,
width_mm = label_mm.width_mm,
height_mm = label_mm.height_mm,
"calibration pattern"
);
let gray = if boundary {
thermark::label::make_boundary_label(lp, pixels_per_mm, None)?
} else {
thermark::label::make_calibration_label(lp, safe, pixels_per_mm, None)?
};
Ok((gray, (lp, safe, pixels_per_mm)))
})
.await?;
if boundary {
println!("OK — boundary probe printed ({label})");
print_boundary_legend(lp, pixels_per_mm);
return Ok(());
}
println!("OK — calibration printed ({label})");
print_calibration_legend(lp, safe, pixels_per_mm);
Ok(())
}