Media

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

#images#photos#branding#uploads

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 error

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.

watermark_with(photo: str, logo: str, out: str, options: any) → anyor an error

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.

watermark_many(photos: list, logo: str, dir: str, options: any) → listor an error

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.

Get it

$ quillo install watermark               # watermark.qwer into this folder$ quillo install watermark --example     # and its example, watermark.qwe

Or 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.png

Then, in your program, all of it or only what you call:

main.qwe
use watermark.qweruse watermark, watermark_with from watermark      // Quillo 0.6: only these

It uses this crate, which cargo fetches the first time the plugin is built:

[dependencies]
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "webp", "gif"] }

Example

watermark.qwe
// 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

watermark.qwer
//! 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)}
EXIF 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.imagesphotosplacesuploads7 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.imagesphotosprivacyuploads6 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.imagesphotosuploadsediting7 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.ocrimagesphotostextdocumentsreceiptsinvoicesformsuploads13 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.imagesphotosfontssocial7 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.imagesphotossocial7 views · 7 downloads
Thread
88Useful? Like it, or add to its thread below.

Thread 0 posts

On Watermark: questions, tips, corrections, and code that uses it.

No posts yet. The first one opens the thread.