Watermark
A logo laid over photos at a corner or the centre, or tiled across, with the opacity you choose, for publishing pictures with your mark.
Version 1.0.0 · 3 functions · 1 crate
Functions
What use watermark.qwer gives a program, with the types Quillo sees. To take only some, or under other names: use watermark, watermark_with from watermark (Quillo 0.6).
watermark(photo: str, logo: str, out: str, position: str, opacity: float) → anyor an errorThe logo (a PNG with transparency, ideally) laid on photo and saved at out (.jpg, .png or .webp; folders made): { width, height }. position is "bottom-right", "bottom-left", "top-right", "top-left" or "center"; opacity from 0 to 1. The logo is a fifth of the photo's width, 3% in from the edges.
watermark_with(photo: str, logo: str, out: str, options: any) → anyor an errorThe same with options: position, opacity (0.5), scale (the logo's width as a part of the photo's, 0.2), margin (as a part of the shorter side, 0.03), and tile true to repeat the logo across the whole photo, as stock-photo sites do.
watermark_many(photos: list, logo: str, dir: str, options: any) → listor an errorEach of photos marked the same way and saved in the folder dir under its own file name: the paths written. Up to 1,000 photos.
Get it
$ quillo install watermark # watermark.qwer into this folder$ quillo install watermark --example # and its example, watermark.qweOr with curl:
$ curl -O https://quillolang.org/plugins/files/watermark.qwer$ curl -O https://quillolang.org/plugins/files/watermark.qwe$ curl -O https://quillolang.org/plugins/files/watermark_sample.jpg$ curl -O https://quillolang.org/plugins/files/watermark_sample.pngThen, in your program, all of it or only what you call:
use watermark.qweruse watermark, watermark_with from watermark // Quillo 0.6: only theseIt uses this crate, which cargo fetches the first time the plugin is built:
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }Example
// Photos marked with the café's logo before they go on the website.//// quillo run watermark.qwe then open http://localhost:8080/photo.jpguse "watermark.qwer"route GET /photo.jpg { watermark("watermark_sample.jpg", "watermark_sample.png", "data/marked/photo.jpg", "bottom-right", 0.6)? file("data/marked/photo.jpg")}route GET /stock.jpg { watermark_with("watermark_sample.jpg", "watermark_sample.png", "data/marked/stock.jpg", { tile: true, opacity: 0.25, scale: 0.12 })? file("data/marked/stock.jpg")}route GET /batch = { corner: watermark("watermark_sample.jpg", "watermark_sample.png", "data/marked/photo.jpg", "top-left", 0.6)?, many: watermark_many(["watermark_sample.jpg"], "watermark_sample.png", "data/marked/all", { position: "center", opacity: 0.4 })?, wrong: is_err(watermark("watermark_sample.jpg", "watermark_sample.png", "data/marked/x.jpg", "middle", 0.5)),}$ quillo run watermark.qwe$ curl 'localhost:8080/batch'$ curl 'localhost:8080/photo.jpg'Source
//! A logo laid over photos at a corner or the centre, or tiled across, with the opacity you choose: for publishing pictures with your mark on them.//! version: 1.0.0//!//! [dependencies]//! image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }use image::{DynamicImage, ImageDecoder, RgbaImage};use serde_json::{json, Value};fn reader(path: &str) -> Result<image::ImageReader<std::io::BufReader<std::fs::File>>, String> { let mut r = image::ImageReader::open(path).map_err(|e| format!("{}: {}", path, e))?.with_guessed_format().map_err(|e| format!("{}: {}", path, e))?; // A small file claiming to be enormous is refused before anything is allocated. let mut l = image::Limits::default(); l.max_image_width = Some(12_000); l.max_image_height = Some(12_000); l.max_alloc = Some(512 * 1024 * 1024); r.limits(l); Ok(r)}// The picture turned upright by its EXIF orientation, as phones expect.fn open(path: &str) -> Result<DynamicImage, String> { let mut dec = reader(path)?.into_decoder().map_err(|e| format!("{}: {}", path, e))?; let o = dec.orientation().unwrap_or(image::metadata::Orientation::NoTransforms); let mut img = DynamicImage::from_decoder(dec).map_err(|e| format!("{}: {}", path, e))?; img.apply_orientation(o); Ok(img)}// Saved in the format its extension names: .jpg (quality 88), .png, .webp (lossless) or .gif; folders are made.fn save(img: &DynamicImage, path: &str) -> Result<(), String> { if let Some(dir) = std::path::Path::new(path).parent().filter(|d| !d.as_os_str().is_empty()) { std::fs::create_dir_all(dir).map_err(|e| format!("{}: {}", dir.display(), e))?; } let ext = path.rsplit_once('.').map(|(_, e)| e.to_lowercase()).unwrap_or_default(); let err = |e: image::ImageError| format!("{}: {}", path, e); match ext.as_str() { "jpg" | "jpeg" => { let f = std::fs::File::create(path).map_err(|e| format!("{}: {}", path, e))?; image::codecs::jpeg::JpegEncoder::new_with_quality(std::io::BufWriter::new(f), 88).encode_image(&img.to_rgb8()).map_err(err) } "png" => img.save_with_format(path, image::ImageFormat::Png).map_err(err), "webp" => DynamicImage::ImageRgba8(img.to_rgba8()).save_with_format(path, image::ImageFormat::WebP).map_err(err), "gif" => DynamicImage::ImageRgba8(img.to_rgba8()).save_with_format(path, image::ImageFormat::Gif).map_err(err), _ => Err(format!("{}: save as .jpg, .png, .webp or .gif", path)), }}struct Opts { position: String, opacity: f32, scale: f32, margin: f32, tile: bool,}fn opts(o: &Value) -> Opts { Opts { position: o.get("position").and_then(Value::as_str).unwrap_or("bottom-right").to_lowercase(), opacity: o.get("opacity").and_then(Value::as_f64).unwrap_or(0.5).clamp(0.0, 1.0) as f32, scale: o.get("scale").and_then(Value::as_f64).unwrap_or(0.2).clamp(0.01, 1.0) as f32, margin: o.get("margin").and_then(Value::as_f64).unwrap_or(0.03).clamp(0.0, 0.4) as f32, tile: o.get("tile").and_then(Value::as_bool).unwrap_or(false), }}// The logo blended onto the photo at (x, y), its own transparency times `opacity`.fn blend(base: &mut RgbaImage, logo: &RgbaImage, x: i64, y: i64, opacity: f32) { for (lx, ly, p) in logo.enumerate_pixels() { let (bx, by) = (x + lx as i64, y + ly as i64); if bx < 0 || by < 0 || bx >= base.width() as i64 || by >= base.height() as i64 { continue; } let a = p.0[3] as f32 / 255.0 * opacity; if a <= 0.0 { continue; } let q = base.get_pixel_mut(bx as u32, by as u32); for c in 0..3 { q.0[c] = (p.0[c] as f32 * a + q.0[c] as f32 * (1.0 - a)).round() as u8; } }}fn mark(photo: &str, logo: &str, out: &str, o: &Opts) -> Result<Value, String> { let img = open(photo)?; let mut base = img.to_rgba8(); let (w, h) = base.dimensions(); let lg = open(logo)?; let lw = ((w as f32 * o.scale).round() as u32).max(1); let lh = ((lg.height() as f32 * lw as f32 / lg.width().max(1) as f32).round() as u32).max(1); let small = lg.resize_exact(lw, lh, image::imageops::FilterType::Lanczos3).to_rgba8(); let m = (w.min(h) as f32 * o.margin).round() as i64; let (lw, lh, w, h) = (lw as i64, lh as i64, w as i64, h as i64); if o.tile { // A grid of logos, every other row shifted, across the whole picture. let (sx, sy) = (lw * 2, lh * 2); let mut row = 0; let mut y = -lh / 2; while y < h { let mut x = if row % 2 == 0 { 0 } else { lw }; while x < w { blend(&mut base, &small, x, y, o.opacity); x += sx; } y += sy; row += 1; } } else { let (x, y) = match o.position.as_str() { "top-left" => (m, m), "top-right" => (w - lw - m, m), "bottom-left" => (m, h - lh - m), "center" | "centre" => ((w - lw) / 2, (h - lh) / 2), "bottom-right" => (w - lw - m, h - lh - m), p => return Err(format!("the position is top-left, top-right, bottom-left, bottom-right or center, not `{}`", p)), }; blend(&mut base, &small, x, y, o.opacity); } let out_img = if img.color().has_alpha() { DynamicImage::ImageRgba8(base) } else { DynamicImage::ImageRgb8(DynamicImage::ImageRgba8(base).to_rgb8()) }; save(&out_img, out)?; Ok(json!({ "width": w, "height": h }))}/// The `logo` (a PNG with transparency, ideally) laid on `photo` and saved at `out` (.jpg, .png or .webp; folders made): `{ width, height }`./// `position` is "bottom-right", "bottom-left", "top-right", "top-left" or "center"; `opacity` from 0 to 1. The logo is a fifth of the photo's width, 3% in from the edges.pub fn watermark(photo: &str, logo: &str, out: &str, position: &str, opacity: f64) -> Result<Value, String> { mark(photo, logo, out, &opts(&json!({ "position": position, "opacity": opacity })))}/// The same with `options`: `position`, `opacity` (0.5), `scale` (the logo's width as a part of the photo's, 0.2), `margin` (as a part of the shorter side, 0.03),/// and `tile` true to repeat the logo across the whole photo, as stock-photo sites do.pub fn watermark_with(photo: &str, logo: &str, out: &str, options: serde_json::Value) -> Result<Value, String> { mark(photo, logo, out, &opts(&options))}/// Each of `photos` marked the same way and saved in the folder `dir` under its own file name: the paths written. Up to 1,000 photos.pub fn watermark_many(photos: Vec<String>, logo: &str, dir: &str, options: serde_json::Value) -> Result<Vec<String>, String> { if photos.len() > 1000 { return Err("up to 1,000 photos at a time".into()); } let o = opts(&options); let mut out = vec![]; for p in &photos { let name = std::path::Path::new(p).file_name().and_then(|n| n.to_str()).ok_or_else(|| format!("{}: no file name", p))?; let to = format!("{}/{}", dir.trim_end_matches('/'), name); mark(p, logo, &to, &o)?; out.push(to); } Ok(out)}Related
exif.qwer · 1.0.0A photo's EXIF: camera and lens, when it was taken, where (GPS), exposure and orientation, from JPEG, HEIC, PNG and WebP.7 views · 5 downloadsEXIF strip exif_strip.qwer · 1.0.0Photos made safe to publish: GPS, camera and other EXIF, XMP, IPTC and comments taken out of JPEG, PNG and WebP, without re-encoding.6 views · 7 downloadsImage edits image_ops.qwer · 1.0.0Crop, rotate, flip, grayscale, blur, and phone photos turned upright by their EXIF orientation, saved as JPEG, PNG, WebP or GIF.7 views · 7 downloadsOCR ocr.qwer · 1.0.0Text read from pictures in pure Rust: photos and scans of documents, receipts, labels and screenshots to plain text, row by row or line by line with boxes, parts of the page read by range (pixels or percentages), several form fields at once, only the characters you allow, and phone photos turned upright first.13 views · 9 downloadsCaptions on images caption.qwer · 1.0.0Text drawn on pictures with a built-in font: captions at the top, middle or bottom, wrapped to fit, with an outline or a band behind them, meme-style top and bottom lines, and text measured before drawing.7 views · 7 downloadsCollage collage.qwer · 1.0.0Several pictures made into one: a grid of equal cells with gaps, or justified rows that keep each picture's shape, on your background.7 views · 7 downloadsThread 0 posts
On Watermark: questions, tips, corrections, and code that uses it.
No posts yet. The first one opens the thread.