one-shot
No files matched your search
@@ -0,0 +1,2 @@
|
||||
[alias]
|
||||
xtask = "run -p xtask --"
|
||||
@@ -0,0 +1,109 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
fuzz:
|
||||
description: "Run the fuzz job"
|
||||
type: boolean
|
||||
default: false
|
||||
fuzz_seconds:
|
||||
description: "Seconds per fuzz target"
|
||||
default: "600"
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_BACKTRACE: 1
|
||||
|
||||
concurrency:
|
||||
group: ci-${{ github.ref }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: test (${{ matrix.os }})
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install Linux system dependencies (egui/winit/rfd)
|
||||
if: runner.os == 'Linux'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y --no-install-recommends \
|
||||
libxkbcommon-dev libwayland-dev libx11-dev libxrandr-dev libxi-dev \
|
||||
libgl1-mesa-dev libgtk-3-dev
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
components: clippy, rustfmt
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
|
||||
- name: Format
|
||||
if: runner.os == 'Linux'
|
||||
run: cargo fmt --all -- --check
|
||||
|
||||
- name: Test
|
||||
run: cargo test --workspace
|
||||
|
||||
- name: Clippy
|
||||
run: cargo clippy --workspace --all-targets -- -D warnings
|
||||
|
||||
- name: Dependency layering
|
||||
run: cargo xtask layers
|
||||
|
||||
wasm:
|
||||
name: wasm32 check
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: wasm32-unknown-unknown
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
key: wasm32
|
||||
- name: Check wasm-safe crates
|
||||
run: cargo xtask wasm
|
||||
|
||||
fuzz:
|
||||
name: fuzz (${{ matrix.target.crate }}/${{ matrix.target.name }})
|
||||
# Placeholder: manual trigger only until a nightly schedule is enabled.
|
||||
if: github.event_name == 'workflow_dispatch' && inputs.fuzz
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- { crate: psd, name: parse }
|
||||
- { crate: psd, name: descriptor }
|
||||
- { crate: codecs, name: decode_any }
|
||||
- { crate: codecs, name: decode_png }
|
||||
- { crate: codecs, name: decode_jpeg }
|
||||
- { crate: codecs, name: decode_tiff }
|
||||
- { crate: codecs, name: decode_exr }
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: dtolnay/rust-toolchain@nightly
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: crates/${{ matrix.target.crate }}/fuzz
|
||||
- name: Install cargo-fuzz
|
||||
run: cargo install cargo-fuzz --locked
|
||||
- name: Fuzz
|
||||
working-directory: crates/${{ matrix.target.crate }}
|
||||
run: cargo fuzz run ${{ matrix.target.name }} -- -max_total_time=${{ inputs.fuzz_seconds }}
|
||||
- name: Upload crash artifacts
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: fuzz-${{ matrix.target.crate }}-${{ matrix.target.name }}
|
||||
path: crates/${{ matrix.target.crate }}/fuzz/artifacts
|
||||
@@ -1 +1,6 @@
|
||||
*~
|
||||
plan/
|
||||
/target
|
||||
/dist
|
||||
/corpus
|
||||
/log
|
||||
@@ -0,0 +1,62 @@
|
||||
# AGENTS.md: guide for AI agents and contributors
|
||||
|
||||
Photocraft is an open-source, native, Photoshop-comparable image editor written in **Rust only** (no JavaScript or TypeScript). Read this file first, then `docs/`.
|
||||
|
||||
## 1. Orientation (5 minutes)
|
||||
|
||||
| Read | Why |
|
||||
|---|---|
|
||||
| `docs/architecture.md` | Crate map, dependency layers, the engine/UI seam, document model |
|
||||
| `docs/development.md` | Build, test, run, drive the app programmatically, debug tricks |
|
||||
| `docs/contributing.md` | Rules: clean-room, tests, layering, style, commits |
|
||||
| `docs/control-protocol.md` | JSON control channel: how agents drive and screenshot the running app |
|
||||
| `docs/ui-design.md` | Design tokens, themes, widgets, and how to match Photoshop's look |
|
||||
| `crates/<name>/README.md` (where present) | Public API of that crate |
|
||||
|
||||
## 2. Workspace map
|
||||
|
||||
```text
|
||||
crates/
|
||||
geom color raster L0 foundation (runtime pixel formats, COW tiles, blend math)
|
||||
psd codecs L0 standalone format crates (no workspace deps; publishable)
|
||||
doc L1 document model (layers, masks, adjustments, effects: pure data)
|
||||
ops paint L2 history/undo, brush engine
|
||||
compose L3 CPU reference compositor (the oracle for GPU work)
|
||||
io L4 document <-> PSD / flat formats
|
||||
engine L5 Session + command registry (every action is a command)
|
||||
ui-egui L6 egui shell (thin: all actions go through the engine)
|
||||
testkit test helpers
|
||||
apps/
|
||||
photocraft desktop app (eframe/wgpu), TCP control server
|
||||
photocraft-cli headless CLI
|
||||
xtask/ cargo xtask layers | wasm | ci | stats | corpus
|
||||
```
|
||||
|
||||
**Layering is enforced** by `cargo xtask layers`. A crate may depend only on lower layers. `psd` and `codecs` depend on nothing in the workspace. Nothing below `ui-egui` may use egui, eframe, winit or rfd.
|
||||
|
||||
## 3. Golden rules
|
||||
|
||||
1. **Everything is a command.** New user-visible behaviour = a command in `crates/engine/src/commands.rs` (id, label, menu path, shortcut, params doc, `enabled`, `run`) plus tests in `crates/engine/src/tests.rs`. The UI, CLI, control channel and future MCP all dispatch commands by id.
|
||||
2. **No format or colour assumptions.** Bit depth (8/16/32f) and colour model (RGB/Gray/CMYK/Lab…) are runtime data. Never introduce a `u8`-only pixel path in public APIs. Test at several depths.
|
||||
3. **Clean-room.** We studied Photon Studio (proprietary) and Photoshop for *behaviour and look only*. Never copy their code, shaders or assets. Implement from public specs (Adobe PSD spec, ISO 32000 blend modes, papers) and observation. Third-party assets must be permissively licensed and recorded (see `assets/`).
|
||||
4. **Tests are the gate.** Every change comes with tests. Format crates use round-trip, synthetic-generator, oracle and fuzz tests. Keep the real-file PSD corpus results from regressing.
|
||||
5. **The UI is thin and data-driven.** UI state lives in `ui-egui/src/state.rs` (serde), so the control channel can read and drive it. Colours and radii come from `theme::Tokens`, never hard-coded.
|
||||
6. **Verify UI changes visually.** Launch the app with `--control`, drive it, take `ui.screenshot`, and look at the PNG. See `docs/development.md`.
|
||||
7. **Never break wasm.** L0–L6 must `cargo check --target wasm32-unknown-unknown` (run `cargo xtask wasm`).
|
||||
|
||||
## 4. Before you finish a task
|
||||
|
||||
```sh
|
||||
cargo test -p <crates you touched>
|
||||
cargo clippy -p <crates> --all-targets -- -D warnings
|
||||
cargo xtask layers
|
||||
cargo xtask wasm # if you touched L0–L6
|
||||
```
|
||||
|
||||
Parallel agents: use your own target dir (`CARGO_TARGET_DIR=target/agent-<name>`) to avoid the Cargo build lock, and edit only the crates you own. Keep every `Cargo.toml` valid at all times: the `crates/*` glob means one broken manifest breaks everyone's build.
|
||||
|
||||
## 5. Where things are tracked
|
||||
|
||||
- `plan/` (local, gitignored): research, parity plan, execution plan, estimates.
|
||||
- `docs/roadmap.md`: milestones M0–M12 and their status.
|
||||
- `log/` (local, gitignored): the dev log.
|
||||
@@ -0,0 +1,60 @@
|
||||
[workspace]
|
||||
resolver = "3"
|
||||
members = ["crates/*", "apps/*", "xtask"]
|
||||
default-members = ["crates/*", "apps/photocraft-cli", "xtask"]
|
||||
|
||||
[workspace.package]
|
||||
version = "0.1.0"
|
||||
edition = "2024"
|
||||
license = "MIT OR Apache-2.0"
|
||||
rust-version = "1.90"
|
||||
|
||||
[workspace.dependencies]
|
||||
# internal
|
||||
photocraft-geom = { path = "crates/geom" }
|
||||
photocraft-color = { path = "crates/color" }
|
||||
photocraft-raster = { path = "crates/raster" }
|
||||
photocraft-doc = { path = "crates/doc" }
|
||||
photocraft-ops = { path = "crates/ops" }
|
||||
photocraft-psd = { path = "crates/psd" }
|
||||
photocraft-codecs = { path = "crates/codecs" }
|
||||
photocraft-compose = { path = "crates/compose" }
|
||||
photocraft-paint = { path = "crates/paint" }
|
||||
photocraft-algo = { path = "crates/algo" }
|
||||
photocraft-io = { path = "crates/io" }
|
||||
photocraft-engine = { path = "crates/engine" }
|
||||
photocraft-ui-egui = { path = "crates/ui-egui" }
|
||||
photocraft-testkit = { path = "crates/testkit" }
|
||||
|
||||
# external
|
||||
thiserror = "2"
|
||||
serde = { version = "1", features = ["derive", "rc"] }
|
||||
serde_json = "1"
|
||||
arc-swap = "1"
|
||||
bytemuck = { version = "1", features = ["derive"] }
|
||||
flate2 = { version = "1", default-features = false, features = ["rust_backend"] }
|
||||
image = { version = "0.25", default-features = false }
|
||||
proptest = "1"
|
||||
eframe = { version = "0.36", default-features = false }
|
||||
egui = "0.36"
|
||||
egui_kittest = "0.36"
|
||||
rfd = "0.17"
|
||||
log = "0.4"
|
||||
rayon = "1"
|
||||
|
||||
[workspace.lints.rust]
|
||||
unsafe_code = "deny"
|
||||
|
||||
[workspace.lints.clippy]
|
||||
dbg_macro = "warn"
|
||||
todo = "warn"
|
||||
|
||||
[profile.dev.package."*"]
|
||||
opt-level = 2
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.release]
|
||||
lto = "thin"
|
||||
codegen-units = 1
|
||||
@@ -2,3 +2,16 @@ photocraft
|
||||
===========
|
||||
|
||||
By the artcraft team
|
||||
|
||||
An open-source, native image editor written in Rust, aiming for Photoshop-level capability and ergonomics.
|
||||
|
||||
- **Native and portable:** egui on wgpu (Metal/Vulkan/DX12/WebGPU), for macOS, Windows, Linux and the web. Mobile shells come later.
|
||||
- **Photoshop-grade formats:** a standalone PSD/PSB crate (byte-exact round trips on a real-file corpus), plus 13 raster formats with symmetric read/write at 8/16/32-bit.
|
||||
- **Engine first:** every action is a command, so the UI, CLI and automation (a JSON control channel, MCP next) all drive the same core.
|
||||
|
||||
```sh
|
||||
cargo run --release -p photocraft -- image.psd
|
||||
cargo test --workspace
|
||||
```
|
||||
|
||||
Start with [`AGENTS.md`](AGENTS.md), then [`docs/`](docs/).
|
||||
@@ -0,0 +1,14 @@
|
||||
[package]
|
||||
name = "photocraft-cli"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "photocraft"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
@@ -0,0 +1 @@
|
||||
fn main() {}
|
||||
@@ -0,0 +1,32 @@
|
||||
[package]
|
||||
name = "photocraft-web"
|
||||
description = "Photocraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
publish = false
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "photocraft-web"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
photocraft-engine = { workspace = true }
|
||||
photocraft-io = { workspace = true }
|
||||
photocraft-codecs = { workspace = true }
|
||||
photocraft-doc = { workspace = true }
|
||||
photocraft-ui-egui = { workspace = true }
|
||||
egui = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
# `wgpu` pulls egui-wgpu's defaults: WebGPU plus the WebGL2 fallback.
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||
rfd = { workspace = true }
|
||||
wasm-bindgen = "0.2"
|
||||
wasm-bindgen-futures = "0.4"
|
||||
js-sys = "0.3"
|
||||
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "CssStyleDeclaration"] }
|
||||
@@ -0,0 +1,10 @@
|
||||
# Build with: trunk build --release (from apps/photocraft-web)
|
||||
[build]
|
||||
target = "index.html"
|
||||
dist = "../../dist/web"
|
||||
public_url = "./"
|
||||
filehash = true
|
||||
|
||||
[serve]
|
||||
addresses = ["127.0.0.1"]
|
||||
port = 8765
|
||||
@@ -0,0 +1,19 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||
<title>Photocraft</title>
|
||||
<link data-trunk rel="rust" data-bin="photocraft-web" data-wasm-opt="z" data-weak-refs />
|
||||
<style>
|
||||
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #262626; }
|
||||
#photocraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||
#photocraft_loading { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center;
|
||||
color: #b8b8b8; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<canvas id="photocraft_canvas"></canvas>
|
||||
<div id="photocraft_loading">Loading Photocraft…</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,30 @@
|
||||
//! Photocraft in the browser.
|
||||
//!
|
||||
//! Runs the same [`photocraft_ui_egui::PhotocraftApp`] as the desktop app through eframe's web
|
||||
//! runner (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release`
|
||||
//! from this directory; see `docs/development.md` ("Web build").
|
||||
//!
|
||||
//! Differences from the desktop app:
|
||||
//! - no TCP control server (browsers can't listen on sockets);
|
||||
//! - File → Open uses the browser file picker; bytes arrive asynchronously through
|
||||
//! `Services::inbox`;
|
||||
//! - saving/exporting triggers a browser download;
|
||||
//! - dropped files are read asynchronously by `web::WebShell` and delivered through the inbox.
|
||||
//!
|
||||
//! URL query flags: `?cpu` forces the CPU canvas path (same as `PHOTOCRAFT_CPU_CANVAS=1`);
|
||||
//! `?webgl` forces the WebGL2 backend instead of WebGPU.
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
mod web;
|
||||
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn main() {
|
||||
web::start();
|
||||
}
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn main() {
|
||||
eprintln!(
|
||||
"photocraft-web only runs in the browser: build it with `trunk build --release` in apps/photocraft-web"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
//! The browser shell: web `Services`, drag-and-drop, and the eframe web runner.
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
use photocraft_codecs::{ChannelLayout, EncodeOptions, Image};
|
||||
use photocraft_doc::Document;
|
||||
use photocraft_engine::Session;
|
||||
use photocraft_ui_egui::theme::ThemeKind;
|
||||
use photocraft_ui_egui::{PhotocraftApp, Services};
|
||||
use wasm_bindgen::JsCast as _;
|
||||
|
||||
type Inbox = Arc<Mutex<Vec<(String, Vec<u8>)>>>;
|
||||
|
||||
const IMAGE_EXTS: &[&str] = &[
|
||||
"psd", "psb", "png", "jpg", "jpeg", "tif", "tiff", "webp", "gif", "bmp", "tga", "ico", "qoi",
|
||||
"exr", "hdr", "pbm", "pgm", "ppm", "pam", "pfm",
|
||||
];
|
||||
const CANVAS_ID: &str = "photocraft_canvas";
|
||||
|
||||
pub fn start() {
|
||||
eframe::WebLogger::init(log::LevelFilter::Info).ok();
|
||||
wasm_bindgen_futures::spawn_local(async {
|
||||
let Some(document) = web_sys::window().and_then(|w| w.document()) else {
|
||||
log::error!("no document");
|
||||
return;
|
||||
};
|
||||
let Some(canvas) = document
|
||||
.get_element_by_id(CANVAS_ID)
|
||||
.and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok())
|
||||
else {
|
||||
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
|
||||
return;
|
||||
};
|
||||
let q = query();
|
||||
let force_cpu = q.contains("cpu");
|
||||
let mut options = eframe::WebOptions::default();
|
||||
if q.contains("webgl")
|
||||
&& let eframe::egui_wgpu::WgpuSetup::CreateNew(create) =
|
||||
&mut options.wgpu_options.wgpu_setup
|
||||
{
|
||||
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
|
||||
}
|
||||
let result = eframe::WebRunner::new()
|
||||
.start(
|
||||
canvas,
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
PhotocraftApp::setup_context(&cc.egui_ctx, ThemeKind::Pro);
|
||||
let inbox: Inbox = Arc::default();
|
||||
let mut app = PhotocraftApp::new(
|
||||
Session::new(),
|
||||
services(inbox.clone(), cc.egui_ctx.clone()),
|
||||
);
|
||||
app.set_theme(&cc.egui_ctx, ThemeKind::Pro);
|
||||
if let Some(rs) = cc.wgpu_render_state.clone()
|
||||
&& !force_cpu
|
||||
{
|
||||
log::info!(
|
||||
"photocraft-web: wgpu backend {:?}",
|
||||
rs.adapter.get_info().backend
|
||||
);
|
||||
app.set_wgpu(rs);
|
||||
}
|
||||
Ok(Box::new(WebShell { app, inbox }))
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
if let Some(el) = document.get_element_by_id("photocraft_loading") {
|
||||
match result {
|
||||
Ok(()) => el.remove(),
|
||||
Err(e) => el.set_inner_html(&format!("<p>Photocraft failed to start: {e:?}</p><p>A browser with WebGPU or WebGL2 is required.</p>")),
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn query() -> String {
|
||||
web_sys::window()
|
||||
.and_then(|w| w.location().search().ok())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Wraps the app to read dropped files asynchronously (browsers can't read them synchronously,
|
||||
/// so the app's own drop path can't handle them) and feed them through the inbox.
|
||||
struct WebShell {
|
||||
app: PhotocraftApp,
|
||||
inbox: Inbox,
|
||||
}
|
||||
|
||||
impl eframe::App for WebShell {
|
||||
fn logic(&mut self, ctx: &egui::Context, frame: &mut eframe::Frame) {
|
||||
let dropped = ctx.input_mut(|i| std::mem::take(&mut i.raw.dropped_files));
|
||||
for f in dropped {
|
||||
let inbox = self.inbox.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let name = f
|
||||
.path()
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "dropped".into());
|
||||
match f.bytes_async().await {
|
||||
Ok(bytes) => {
|
||||
inbox
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.push((name, bytes));
|
||||
ctx.request_repaint();
|
||||
}
|
||||
Err(e) => log::error!("couldn't read dropped file {name}: {e}"),
|
||||
}
|
||||
});
|
||||
}
|
||||
self.app.logic(ctx, frame);
|
||||
}
|
||||
|
||||
fn ui(&mut self, ui: &mut egui::Ui, frame: &mut eframe::Frame) {
|
||||
self.app.ui(ui, frame);
|
||||
}
|
||||
}
|
||||
|
||||
fn services(inbox: Inbox, ctx: egui::Context) -> Services {
|
||||
let open_inbox = inbox.clone();
|
||||
Services {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| {
|
||||
photocraft_io::import(name, bytes)
|
||||
.map(|r| r.document)
|
||||
.map_err(|e| e.to_string())
|
||||
})),
|
||||
export: Some(Box::new(|doc: &Document, path: &str| {
|
||||
photocraft_io::export(doc, path, &Default::default())
|
||||
.map(|r| r.bytes)
|
||||
.map_err(|e| e.to_string())
|
||||
})),
|
||||
pick_open: Some(Box::new(move || {
|
||||
let inbox = open_inbox.clone();
|
||||
let ctx = ctx.clone();
|
||||
wasm_bindgen_futures::spawn_local(async move {
|
||||
let Some(file) = rfd::AsyncFileDialog::new()
|
||||
.add_filter("Images", IMAGE_EXTS)
|
||||
.pick_file()
|
||||
.await
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let bytes = file.read().await;
|
||||
inbox
|
||||
.lock()
|
||||
.unwrap_or_else(|e| e.into_inner())
|
||||
.push((file.file_name(), bytes));
|
||||
ctx.request_repaint();
|
||||
});
|
||||
None
|
||||
})),
|
||||
// No save dialog on the web: the suggested name becomes the download name.
|
||||
pick_save: Some(Box::new(|suggested: &str| {
|
||||
let name = std::path::Path::new(suggested)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| suggested.to_string());
|
||||
Some(name)
|
||||
})),
|
||||
write: Some(Box::new(|path: &str, bytes: &[u8]| download(path, bytes))),
|
||||
encode_png: Some(Box::new(|w, h, rgba| {
|
||||
let img = Image::from_u8(w, h, ChannelLayout::Rgba, rgba.to_vec())
|
||||
.map_err(|e| e.to_string())?;
|
||||
photocraft_codecs::encode(
|
||||
&img,
|
||||
photocraft_codecs::Format::Png,
|
||||
&EncodeOptions::default(),
|
||||
)
|
||||
.map_err(|e| e.to_string())
|
||||
})),
|
||||
inbox: Some(inbox),
|
||||
}
|
||||
}
|
||||
|
||||
/// Trigger a browser download of `bytes` named after the last component of `path`.
|
||||
fn download(path: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
let js = |e: wasm_bindgen::JsValue| format!("{e:?}");
|
||||
let name = std::path::Path::new(path)
|
||||
.file_name()
|
||||
.map(|n| n.to_string_lossy().to_string())
|
||||
.unwrap_or_else(|| "photocraft".into());
|
||||
let window = web_sys::window().ok_or("no window")?;
|
||||
let document = window.document().ok_or("no document")?;
|
||||
let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes));
|
||||
let opts = web_sys::BlobPropertyBag::new();
|
||||
opts.set_type(mime_for(&name));
|
||||
let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &opts).map_err(js)?;
|
||||
let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(js)?;
|
||||
let a: web_sys::HtmlAnchorElement = document
|
||||
.create_element("a")
|
||||
.map_err(js)?
|
||||
.dyn_into()
|
||||
.map_err(|_| "not an anchor")?;
|
||||
a.set_href(&url);
|
||||
a.set_download(&name);
|
||||
a.style().set_property("display", "none").map_err(js)?;
|
||||
let body = document.body().ok_or("no body")?;
|
||||
body.append_child(&a).map_err(js)?;
|
||||
a.click();
|
||||
a.remove();
|
||||
// Revoke after the click has been dispatched; the download keeps its own reference.
|
||||
let revoke = wasm_bindgen::closure::Closure::once_into_js(move || {
|
||||
web_sys::Url::revoke_object_url(&url).ok();
|
||||
});
|
||||
window
|
||||
.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000)
|
||||
.map_err(js)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn mime_for(name: &str) -> &'static str {
|
||||
match name
|
||||
.rsplit('.')
|
||||
.next()
|
||||
.map(str::to_ascii_lowercase)
|
||||
.as_deref()
|
||||
{
|
||||
Some("png") => "image/png",
|
||||
Some("jpg" | "jpeg") => "image/jpeg",
|
||||
Some("tif" | "tiff") => "image/tiff",
|
||||
Some("webp") => "image/webp",
|
||||
Some("gif") => "image/gif",
|
||||
Some("psd" | "psb") => "image/vnd.adobe.photoshop",
|
||||
_ => "application/octet-stream",
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
[package]
|
||||
name = "photocraft"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
default-run = "photocraft"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
photocraft-doc = { workspace = true }
|
||||
photocraft-color = { workspace = true }
|
||||
photocraft-raster = { workspace = true }
|
||||
photocraft-geom = { workspace = true }
|
||||
photocraft-compose = { workspace = true }
|
||||
photocraft-engine = { workspace = true }
|
||||
photocraft-codecs = { workspace = true }
|
||||
photocraft-io = { workspace = true }
|
||||
photocraft-ui-egui = { workspace = true }
|
||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
|
||||
egui = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
log = { workspace = true }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rfd = { workspace = true }
|
||||
@@ -0,0 +1,62 @@
|
||||
//! Localhost JSON-lines control server (one request per line, one reply per line).
|
||||
//! Loopback only. This is the transport the MCP server will wrap.
|
||||
|
||||
use std::io::{BufRead, BufReader, Write};
|
||||
use std::net::{TcpListener, TcpStream};
|
||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||
use std::time::Duration;
|
||||
|
||||
use photocraft_ui_egui::ControlRequest;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
||||
let (tx, rx) = channel::<ControlRequest>();
|
||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||
Ok(l) => l,
|
||||
Err(e) => {
|
||||
eprintln!("photocraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
||||
return rx;
|
||||
}
|
||||
};
|
||||
eprintln!("photocraft: control server listening on 127.0.0.1:{port}");
|
||||
std::thread::spawn(move || {
|
||||
for stream in listener.incoming().flatten() {
|
||||
let tx = tx.clone();
|
||||
let ctx = ctx.clone();
|
||||
std::thread::spawn(move || serve(stream, tx, ctx));
|
||||
}
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
||||
let Ok(read) = stream.try_clone() else { return };
|
||||
let mut out = stream;
|
||||
for line in BufReader::new(read).lines() {
|
||||
let Ok(line) = line else { break };
|
||||
if line.trim().is_empty() {
|
||||
continue;
|
||||
}
|
||||
let reply = match serde_json::from_str::<Value>(&line) {
|
||||
Ok(msg) => {
|
||||
let id = msg.get("id").cloned().unwrap_or(Value::Null);
|
||||
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
|
||||
let params = msg.get("params").cloned().unwrap_or(json!({}));
|
||||
let (req, rrx) = ControlRequest::new(method, params);
|
||||
if tx.send(req).is_err() {
|
||||
break;
|
||||
}
|
||||
ctx.request_repaint();
|
||||
let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
||||
if let Some(o) = r.as_object_mut() {
|
||||
o.insert("id".into(), id);
|
||||
}
|
||||
r
|
||||
}
|
||||
Err(e) => json!({"ok": false, "error": format!("bad JSON: {e}")}),
|
||||
};
|
||||
if writeln!(out, "{reply}").is_err() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! Photocraft desktop app.
|
||||
//!
|
||||
//! Usage: `photocraft [--control <port>] [files…]`
|
||||
//!
|
||||
//! `--control <port>` (or `PHOTOCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server.
|
||||
//! Each line `{"id":1,"method":"ui.inspect","params":{}}` gets a reply line
|
||||
//! `{"id":1,"ok":true,"result":…}`. See `photocraft_ui_egui::control` for the methods.
|
||||
|
||||
mod control_server;
|
||||
mod services;
|
||||
|
||||
use photocraft_engine::Session;
|
||||
use photocraft_ui_egui::PhotocraftApp;
|
||||
|
||||
fn main() -> eframe::Result {
|
||||
let mut control_port: Option<u16> = std::env::var("PHOTOCRAFT_CONTROL_PORT").ok().and_then(|p| p.parse().ok());
|
||||
let mut files = Vec::new();
|
||||
let mut args = std::env::args().skip(1);
|
||||
while let Some(a) = args.next() {
|
||||
match a.as_str() {
|
||||
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
|
||||
"--version" => {
|
||||
println!("photocraft {}", env!("CARGO_PKG_VERSION"));
|
||||
return Ok(());
|
||||
}
|
||||
_ => files.push(a),
|
||||
}
|
||||
}
|
||||
|
||||
let options = eframe::NativeOptions {
|
||||
viewport: egui::ViewportBuilder::default().with_title("Photocraft").with_inner_size([1440.0, 900.0]).with_min_inner_size([760.0, 480.0]).with_drag_and_drop(true).with_fullsize_content_view(true).with_titlebar_shown(false).with_title_shown(false),
|
||||
..Default::default()
|
||||
};
|
||||
eframe::run_native(
|
||||
"Photocraft",
|
||||
options,
|
||||
Box::new(move |cc| {
|
||||
let mut app = PhotocraftApp::new(Session::new(), services::native());
|
||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||
if let Some(rs) = cc.wgpu_render_state.clone()
|
||||
&& std::env::var_os("PHOTOCRAFT_CPU_CANVAS").is_none()
|
||||
{
|
||||
app.set_wgpu(rs);
|
||||
}
|
||||
if let Some(port) = control_port {
|
||||
let rx = control_server::start(port, cc.egui_ctx.clone());
|
||||
app = app.with_control(rx);
|
||||
}
|
||||
for f in files {
|
||||
match std::fs::read(&f) {
|
||||
Ok(bytes) => {
|
||||
let name = std::path::Path::new(&f).file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or(f.clone());
|
||||
if let Err(e) = app.open_bytes(&name, &bytes) {
|
||||
eprintln!("photocraft: {f}: {e}");
|
||||
} else if let Some(st) = app.session.active_mut() {
|
||||
st.path = Some(f.clone());
|
||||
}
|
||||
}
|
||||
Err(e) => eprintln!("photocraft: {f}: {e}"),
|
||||
}
|
||||
}
|
||||
Ok(Box::new(app))
|
||||
}),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
//! Native platform services: file dialogs (rfd), filesystem, codecs.
|
||||
|
||||
use photocraft_codecs::{ChannelLayout, EncodeOptions, Image, SampleType as CS};
|
||||
use photocraft_color::{ColorMode, SampleType};
|
||||
use photocraft_doc::{Document, Layer, Size};
|
||||
use photocraft_geom::Rect;
|
||||
use photocraft_ui_egui::Services;
|
||||
|
||||
const IMAGE_EXTS: &[&str] = &["psd", "psb", "png", "jpg", "jpeg", "tif", "tiff", "webp", "gif", "bmp", "tga", "ico", "qoi", "exr", "hdr", "pbm", "pgm", "ppm", "pam", "pfm"];
|
||||
|
||||
pub fn native() -> Services {
|
||||
Services {
|
||||
import: Some(Box::new(|name: &str, bytes: &[u8]| photocraft_io::import(name, bytes).map(|r| r.document).map_err(|e| e.to_string()))),
|
||||
export: Some(Box::new(|doc: &Document, path: &str| photocraft_io::export(doc, path, &Default::default()).map(|r| r.bytes).map_err(|e| e.to_string()))),
|
||||
pick_open: Some(Box::new(|| {
|
||||
let path = rfd::FileDialog::new().add_filter("Images", IMAGE_EXTS).pick_file()?;
|
||||
let bytes = std::fs::read(&path).ok()?;
|
||||
Some((path.to_string_lossy().to_string(), bytes))
|
||||
})),
|
||||
pick_save: Some(Box::new(|suggested: &str| {
|
||||
let p = std::path::Path::new(suggested);
|
||||
let mut d = rfd::FileDialog::new().add_filter("Photoshop", &["psd", "psb"]).add_filter("PNG", &["png"]).add_filter("JPEG", &["jpg"]).add_filter("TIFF", &["tif"]).add_filter("OpenEXR", &["exr"]);
|
||||
if let Some(name) = p.file_name() {
|
||||
d = d.set_file_name(name.to_string_lossy());
|
||||
}
|
||||
Some(d.save_file()?.to_string_lossy().to_string())
|
||||
})),
|
||||
write: Some(Box::new(|path: &str, bytes: &[u8]| std::fs::write(path, bytes).map_err(|e| e.to_string()))),
|
||||
encode_png: Some(Box::new(|w, h, rgba| {
|
||||
let img = Image::from_u8(w, h, ChannelLayout::Rgba, rgba.to_vec()).map_err(|e| e.to_string())?;
|
||||
photocraft_codecs::encode(&img, photocraft_codecs::Format::Png, &EncodeOptions::default()).map_err(|e| e.to_string())
|
||||
})),
|
||||
inbox: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Flat-image import via photocraft-codecs (kept for reference/tests; the app uses photocraft-io).
|
||||
#[allow(dead_code)]
|
||||
pub fn import_flat(name: &str, bytes: &[u8]) -> Result<Document, String> {
|
||||
let img = photocraft_codecs::decode(bytes).map_err(|e| e.to_string())?;
|
||||
let (w, h) = (img.width(), img.height());
|
||||
let depth = match img.sample_type() {
|
||||
CS::U8 => SampleType::U8,
|
||||
CS::U16 => SampleType::U16,
|
||||
_ => SampleType::F32,
|
||||
};
|
||||
let gray = matches!(img.layout(), ChannelLayout::Gray | ChannelLayout::GrayA);
|
||||
let cmyk = matches!(img.layout(), ChannelLayout::Cmyk | ChannelLayout::CmykA);
|
||||
let mode = if gray { ColorMode::Grayscale } else if cmyk { ColorMode::Cmyk } else { ColorMode::Rgb };
|
||||
let target = match mode {
|
||||
ColorMode::Grayscale => ChannelLayout::GrayA,
|
||||
ColorMode::Cmyk => ChannelLayout::CmykA,
|
||||
_ => ChannelLayout::Rgba,
|
||||
};
|
||||
let sample = match depth {
|
||||
SampleType::U8 => CS::U8,
|
||||
SampleType::U16 => CS::U16,
|
||||
SampleType::F32 => CS::F32,
|
||||
};
|
||||
let conv = img.convert(target, sample);
|
||||
let stem = std::path::Path::new(name).file_stem().map(|s| s.to_string_lossy().to_string()).unwrap_or(name.to_string());
|
||||
let mut doc = Document::new(stem, Size::new(w, h), mode, depth);
|
||||
doc.icc_profile = img.icc.clone().map(std::sync::Arc::new);
|
||||
if let Some((x, _)) = img.meta.dpi {
|
||||
doc.resolution_dpi = x;
|
||||
}
|
||||
let mut layer = Layer::raster("Background", doc.pixel_format());
|
||||
let data = conv.to_normalized();
|
||||
layer.surface_mut().unwrap().write_region(Rect::from_xywh(0, 0, w, h), &data);
|
||||
doc.layers.push(layer);
|
||||
Ok(doc)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn export_flat(doc: &Document, path: &str) -> Result<Vec<u8>, String> {
|
||||
let format = photocraft_codecs::from_extension(path).ok_or_else(|| format!("unknown file type for {path}"))?;
|
||||
let buf = photocraft_compose::flatten(doc);
|
||||
let (w, h) = (buf.rect.width(), buf.rect.height());
|
||||
let data: Vec<f32> = buf.px.iter().flat_map(|p| *p).collect();
|
||||
let img = match doc.depth {
|
||||
SampleType::U8 => Image::from_u8(w, h, ChannelLayout::Rgba, buf.to_rgba8().pixels),
|
||||
SampleType::U16 => Image::from_u16(w, h, ChannelLayout::Rgba, &data.iter().map(|v| (v.clamp(0.0, 1.0) * 65535.0 + 0.5) as u16).collect::<Vec<_>>()),
|
||||
SampleType::F32 => Image::from_f32(w, h, ChannelLayout::Rgba, &data),
|
||||
}
|
||||
.map_err(|e| e.to_string())?;
|
||||
let img = match &doc.icc_profile {
|
||||
Some(icc) if doc.mode == ColorMode::Rgb => img.with_icc(Some((**icc).clone())),
|
||||
_ => img,
|
||||
};
|
||||
photocraft_codecs::encode(&img, format, &EncodeOptions::default()).map_err(|e| e.to_string())
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION AND CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,43 @@
|
||||
ISC License
|
||||
|
||||
Copyright (c) 2026 Lucide Icons and Contributors
|
||||
|
||||
Permission to use, copy, modify, and/or distribute this software for any
|
||||
purpose with or without fee is hereby granted, provided that the above
|
||||
copyright notice and this permission notice appear in all copies.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||
|
||||
---
|
||||
|
||||
The following Lucide icons are derived from the Feather project:
|
||||
|
||||
airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
|
||||
|
||||
The MIT License (MIT) (for the icons listed above)
|
||||
|
||||
Copyright (c) 2013-present Cole Bemis
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect x="2" y="4" width="20" height="16" rx="2" />
|
||||
<path d="M10 4v4" />
|
||||
<path d="M2 8h20" />
|
||||
<path d="M6 4v4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M8 3 4 7l4 4" />
|
||||
<path d="M4 7h16" />
|
||||
<path d="m16 21 4-4-4-4" />
|
||||
<path d="M20 17H4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 313 B |
@@ -0,0 +1,19 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 10.01h.01" />
|
||||
<path d="M10 14.01h.01" />
|
||||
<path d="M14 10.01h.01" />
|
||||
<path d="M14 14.01h.01" />
|
||||
<path d="M18 6v12" />
|
||||
<path d="M6 6v12" />
|
||||
<rect x="2" y="6" width="20" height="12" rx="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 424 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="15" cy="9" r="7" />
|
||||
<circle cx="9" cy="15" r="7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 276 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m16 22-1-4" />
|
||||
<path d="M19 14a1 1 0 0 0 1-1v-1a2 2 0 0 0-2-2h-3a1 1 0 0 1-1-1V4a2 2 0 0 0-4 0v5a1 1 0 0 1-1 1H6a2 2 0 0 0-2 2v1a1 1 0 0 0 1 1" />
|
||||
<path d="M19 14H5l-1.973 6.767A1 1 0 0 0 4 22h16a1 1 0 0 0 .973-1.233z" />
|
||||
<path d="m8 22 1-4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 470 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m11 10 3 3" />
|
||||
<path d="M6.5 21A3.5 3.5 0 1 0 3 17.5a2.62 2.62 0 0 1-.708 1.792A1 1 0 0 0 3 21z" />
|
||||
<path d="M9.969 17.031 21.378 5.624a1 1 0 0 0-3.002-3.002L6.967 14.031" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 398 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 6 9 17l-5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 239 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 237 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m11 17-5-5 5-5" />
|
||||
<path d="m18 17-5-5 5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 17 5-5-5-5" />
|
||||
<path d="m13 17 5-5-5-5" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 267 B |
@@ -0,0 +1,20 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10.1 2.182a10 10 0 0 1 3.8 0" />
|
||||
<path d="M13.9 21.818a10 10 0 0 1-3.8 0" />
|
||||
<path d="M17.609 3.721a10 10 0 0 1 2.69 2.7" />
|
||||
<path d="M2.182 13.9a10 10 0 0 1 0-3.8" />
|
||||
<path d="M20.279 17.609a10 10 0 0 1-2.7 2.69" />
|
||||
<path d="M21.818 10.1a10 10 0 0 1 0 3.8" />
|
||||
<path d="M3.721 6.391a10 10 0 0 1 2.7-2.69" />
|
||||
<path d="M6.391 20.279a10 10 0 0 1-2.69-2.7" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 590 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 279 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 6v6l4 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 271 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="m16.24 7.76-1.804 5.411a2 2 0 0 1-1.265 1.265L7.76 16.24l1.804-5.411a2 2 0 0 1 1.265-1.265z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 18a6 6 0 0 0 0-12v12z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="14" height="14" x="8" y="8" rx="2" ry="2" />
|
||||
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 2v14a2 2 0 0 0 2 2h14" />
|
||||
<path d="M18 22V8a2 2 0 0 0-2-2H2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 288 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 22a7 7 0 0 0 7-7c0-2-1-3.9-3-5.5s-3.5-4-4-6.5c-.5 2.5-2 4.9-4 6.5C6 11.1 5 13 5 15a7 7 0 0 0 7 7z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 325 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="1" />
|
||||
<circle cx="19" cy="12" r="1" />
|
||||
<circle cx="5" cy="12" r="1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 312 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M21 21H8a2 2 0 0 1-1.42-.587l-3.994-3.999a2 2 0 0 1 0-2.828l10-10a2 2 0 0 1 2.829 0l5.999 6a2 2 0 0 1 0 2.828L12.834 21" />
|
||||
<path d="m5.082 11.09 8.828 8.828" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 383 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49" />
|
||||
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242" />
|
||||
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143" />
|
||||
<path d="m2 2 20 20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 504 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M2.062 12.348a1 1 0 0 1 0-.696 10.75 10.75 0 0 1 19.876 0 1 1 0 0 1 0 .696 10.75 10.75 0 0 1-19.876 0" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 360 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z" />
|
||||
<path d="M14 2v5a1 1 0 0 0 1 1h5" />
|
||||
<path d="M9 15h6" />
|
||||
<path d="M12 18v-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 421 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m6 14 1.5-2.9A2 2 0 0 1 9.24 10H20a2 2 0 0 1 1.94 2.5l-1.54 6a2 2 0 0 1-1.95 1.5H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h3.9a2 2 0 0 1 1.69.9l.81 1.2a2 2 0 0 0 1.67.9H18a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 401 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 10v6" />
|
||||
<path d="M9 13h6" />
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 342 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m12 14 4-4" />
|
||||
<path d="M3.34 19a10 10 0 1 1 17.32 0" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 278 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M3 9h18" />
|
||||
<path d="M3 15h18" />
|
||||
<path d="M9 3v18" />
|
||||
<path d="M15 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 11V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2" />
|
||||
<path d="M14 10V4a2 2 0 0 0-2-2a2 2 0 0 0-2 2v2" />
|
||||
<path d="M10 10.5V6a2 2 0 0 0-2-2a2 2 0 0 0-2 2v8" />
|
||||
<path d="M18 8a2 2 0 1 1 4 0v6a8 8 0 0 1-8 8h-2c-2.8 0-4.5-.86-5.99-2.34l-3.6-3.6a2 2 0 0 1 2.83-2.82L7 15" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 483 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" ry="2" />
|
||||
<circle cx="9" cy="9" r="2" />
|
||||
<path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 358 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 16v-4" />
|
||||
<path d="M12 8h.01" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 294 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3.704 14.467a10 8 0 1 1 3.115 2.375" />
|
||||
<path d="M7 22a5 5 0 0 1-2-3.994" />
|
||||
<circle cx="5" cy="16" r="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 333 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z" />
|
||||
<path d="M2 12a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 12" />
|
||||
<path d="M2 17a1 1 0 0 0 .58.91l8.6 3.91a2 2 0 0 0 1.65 0l8.58-3.9A1 1 0 0 0 22 17" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 510 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71" />
|
||||
<path d="M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 359 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 9.9-1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="11" x="3" y="11" rx="2" ry="2" />
|
||||
<path d="M7 11V7a5 5 0 0 1 10 0v4" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15 3h6v6" />
|
||||
<path d="m21 3-7 7" />
|
||||
<path d="m3 21 7-7" />
|
||||
<path d="M9 21H3v-6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M22 17a2 2 0 0 1-2 2H6.828a2 2 0 0 0-1.414.586l-2.202 2.202A.71.71 0 0 1 2 21.286V5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 339 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 232 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M20.985 12.486a9 9 0 1 1-9.473-9.472c.405-.022.617.46.402.803a6 6 0 0 0 8.268 8.268c.344-.215.825-.004.803.401" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 334 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M4.037 4.688a.495.495 0 0 1 .651-.651l16 6.5a.5.5 0 0 1-.063.947l-6.124 1.58a2 2 0 0 0-1.438 1.435l-1.579 6.126a.5.5 0 0 1-.947.063z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 356 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 2v20" />
|
||||
<path d="m15 19-3 3-3-3" />
|
||||
<path d="m19 9 3 3-3 3" />
|
||||
<path d="M2 12h20" />
|
||||
<path d="m5 9-3 3 3 3" />
|
||||
<path d="m9 5 3-3 3 3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<polygon points="3 11 22 2 13 21 11 13 3 11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 258 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 7 6 2" />
|
||||
<path d="M18.992 12H2.041" />
|
||||
<path d="M21.145 18.38A3.34 3.34 0 0 1 20 16.5a3.3 3.3 0 0 1-1.145 1.88c-.575.46-.855 1.02-.855 1.595A2 2 0 0 0 20 22a2 2 0 0 0 2-2.025c0-.58-.285-1.13-.855-1.595" />
|
||||
<path d="m8.5 4.5 2.148-2.148a1.205 1.205 0 0 1 1.704 0l7.296 7.296a1.205 1.205 0 0 1 0 1.704l-7.592 7.592a3.615 3.615 0 0 1-5.112 0l-3.888-3.888a3.615 3.615 0 0 1 0-5.112L5.67 7.33" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 622 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 22a1 1 0 0 1 0-20 10 9 0 0 1 10 9 5 5 0 0 1-5 5h-2.25a1.75 1.75 0 0 0-1.4 2.8l.3.4a1.75 1.75 0 0 1-1.4 2.8z" />
|
||||
<circle cx="13.5" cy="6.5" r=".5" fill="currentColor" />
|
||||
<circle cx="17.5" cy="10.5" r=".5" fill="currentColor" />
|
||||
<circle cx="6.5" cy="12.5" r=".5" fill="currentColor" />
|
||||
<circle cx="8.5" cy="7.5" r=".5" fill="currentColor" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 571 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M15 3v18" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
<path d="M3 9h18" />
|
||||
<path d="M9 21V9" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M15.707 21.293a1 1 0 0 1-1.414 0l-1.586-1.586a1 1 0 0 1 0-1.414l5.586-5.586a1 1 0 0 1 1.414 0l1.586 1.586a1 1 0 0 1 0 1.414z" />
|
||||
<path d="m18 13-1.375-6.874a1 1 0 0 0-.746-.776L3.235 2.028a1 1 0 0 0-1.207 1.207L5.35 15.879a1 1 0 0 0 .776.746L13 18" />
|
||||
<path d="m2.3 2.3 7.286 7.286" />
|
||||
<circle cx="11" cy="11" r="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 544 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m12 9-8.414 8.414A2 2 0 0 0 3 18.828v1.344a2 2 0 0 1-.586 1.414A2 2 0 0 1 3.828 21h1.344a2 2 0 0 0 1.414-.586L15 12" />
|
||||
<path d="m18 9 .4.4a1 1 0 1 1-3 3l-3.8-3.8a1 1 0 1 1 3-3l.4.4 3.4-3.4a1 1 0 1 1 3 3z" />
|
||||
<path d="m2 22 .414-.414" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 461 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 12h14" />
|
||||
<path d="M12 5v14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 256 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m15 14 5-5-5-5" />
|
||||
<path d="M20 9H9.5A5.5 5.5 0 0 0 4 14.5A5.5 5.5 0 0 0 9.5 20H13" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 308 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M3 7V5a2 2 0 0 1 2-2h2" />
|
||||
<path d="M17 3h2a2 2 0 0 1 2 2v2" />
|
||||
<path d="M21 17v2a2 2 0 0 1-2 2h-2" />
|
||||
<path d="M7 21H5a2 2 0 0 1-2-2v-2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 366 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="6" cy="6" r="3" />
|
||||
<path d="M8.12 8.12 12 12" />
|
||||
<path d="M20 4 8.12 15.88" />
|
||||
<circle cx="6" cy="18" r="3" />
|
||||
<path d="M14.8 14.8 20 20" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 371 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21 21-4.34-4.34" />
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915" />
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 586 B |
@@ -0,0 +1,21 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 5H3" />
|
||||
<path d="M12 19H3" />
|
||||
<path d="M14 3v4" />
|
||||
<path d="M16 17v4" />
|
||||
<path d="M21 12h-9" />
|
||||
<path d="M21 19h-5" />
|
||||
<path d="M21 5h-7" />
|
||||
<path d="M8 10v4" />
|
||||
<path d="M8 12H3" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 422 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z" />
|
||||
<path d="M20 2v4" />
|
||||
<path d="M22 4h-4" />
|
||||
<circle cx="4" cy="20" r="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 567 B |
@@ -0,0 +1,24 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M5 3a2 2 0 0 0-2 2" />
|
||||
<path d="M19 3a2 2 0 0 1 2 2" />
|
||||
<path d="M21 19a2 2 0 0 1-2 2" />
|
||||
<path d="M5 21a2 2 0 0 1-2-2" />
|
||||
<path d="M9 3h1" />
|
||||
<path d="M9 21h1" />
|
||||
<path d="M14 3h1" />
|
||||
<path d="M14 21h1" />
|
||||
<path d="M3 9v1" />
|
||||
<path d="M21 9v1" />
|
||||
<path d="M3 14v1" />
|
||||
<path d="M21 14v1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 532 B |
@@ -0,0 +1,13 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<rect width="18" height="18" x="3" y="3" rx="2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 261 B |
@@ -0,0 +1,18 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 22a2 2 0 0 1-2-2" />
|
||||
<path d="M16 22h-2" />
|
||||
<path d="M16 4a2 2 0 0 0-2-2H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-5a2 2 0 0 1 2-2h5a1 1 0 0 0 1-1z" />
|
||||
<path d="M20 8a2 2 0 0 1 2 2" />
|
||||
<path d="M22 14v2" />
|
||||
<path d="M22 20a2 2 0 0 1-2 2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 482 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M14 13V8.5C14 7 15 7 15 5a3 3 0 0 0-6 0c0 2 1 2 1 3.5V13" />
|
||||
<path d="M20 15.5a2.5 2.5 0 0 0-2.5-2.5h-11A2.5 2.5 0 0 0 4 15.5V17a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1z" />
|
||||
<path d="M5 22h14" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 410 B |
@@ -0,0 +1,21 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="4" />
|
||||
<path d="M12 2v2" />
|
||||
<path d="M12 20v2" />
|
||||
<path d="m4.93 4.93 1.41 1.41" />
|
||||
<path d="m17.66 17.66 1.41 1.41" />
|
||||
<path d="M2 12h2" />
|
||||
<path d="M20 12h2" />
|
||||
<path d="m6.34 17.66-1.41 1.41" />
|
||||
<path d="m19.07 4.93-1.41 1.41" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 485 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M11 17a4 4 0 0 1-8 0V5a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2Z" />
|
||||
<path d="M16.7 13H19a2 2 0 0 1 2 2v4a2 2 0 0 1-2 2H7" />
|
||||
<path d="M 7 17h.01" />
|
||||
<path d="m11 8 2.3-2.3a2.4 2.4 0 0 1 3.404.004L18.6 7.6a2.4 2.4 0 0 1 .026 3.434L9.9 19.8" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 458 B |
@@ -0,0 +1,17 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M10 11v6" />
|
||||
<path d="M14 11v6" />
|
||||
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6" />
|
||||
<path d="M3 6h18" />
|
||||
<path d="M8 6V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 389 B |
@@ -0,0 +1,15 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M12 4v16" />
|
||||
<path d="M4 7V5a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v2" />
|
||||
<path d="M9 20h6" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 310 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M9 14 4 9l5-5" />
|
||||
<path d="M4 9h10.5a5.5 5.5 0 0 1 5.5 5.5a5.5 5.5 0 0 1-5.5 5.5H11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 309 B |
@@ -0,0 +1,20 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="m21.64 3.64-1.28-1.28a1.21 1.21 0 0 0-1.72 0L2.36 18.64a1.21 1.21 0 0 0 0 1.72l1.28 1.28a1.2 1.2 0 0 0 1.72 0L21.64 5.36a1.2 1.2 0 0 0 0-1.72" />
|
||||
<path d="m14 7 3 3" />
|
||||
<path d="M5 6v4" />
|
||||
<path d="M19 14v4" />
|
||||
<path d="M10 2v2" />
|
||||
<path d="M7 8H3" />
|
||||
<path d="M21 16h-4" />
|
||||
<path d="M11 3H9" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 529 B |
@@ -0,0 +1,14 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18" />
|
||||
<path d="m6 6 12 12" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 260 B |
@@ -0,0 +1,16 @@
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
>
|
||||
<circle cx="11" cy="11" r="8" />
|
||||
<line x1="21" x2="16.65" y1="21" y2="16.65" />
|
||||
<line x1="11" x2="11" y1="8" y2="14" />
|
||||
<line x1="8" x2="14" y1="11" y2="11" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 376 B |
@@ -0,0 +1,23 @@
|
||||
[package]
|
||||
name = "photocraft-algo"
|
||||
description = "Depth-agnostic, tile-aware CPU image filters (blur, sharpen, noise, distort, stylize)"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
rust-version.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
photocraft-geom = { workspace = true }
|
||||
photocraft-color = { workspace = true }
|
||||
photocraft-raster = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
# Parallel tiles on native; wasm builds stay single-threaded.
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
rayon = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
serde_json = { workspace = true }
|
||||
@@ -0,0 +1,22 @@
|
||||
//! Timing: Gaussian Blur radius 10 on a 6016×6016 RGBA8 layer.
|
||||
//! `cargo run --release -p photocraft-algo --example bench_gaussian`
|
||||
use photocraft_algo::{FilterParams, apply, output_area};
|
||||
use photocraft_color::PixelFormat;
|
||||
use photocraft_geom::Rect;
|
||||
use photocraft_raster::Surface;
|
||||
|
||||
fn main() {
|
||||
let n: i32 = std::env::args().nth(1).and_then(|v| v.parse().ok()).unwrap_or(6016);
|
||||
let radius: f32 = std::env::args().nth(2).and_then(|v| v.parse().ok()).unwrap_or(10.0);
|
||||
let r = Rect::new(0, 0, n, n);
|
||||
let mut s = Surface::new(PixelFormat::RGBA8);
|
||||
let row: Vec<u8> = (0..n).flat_map(|x| [(x % 256) as u8, (x / 7 % 256) as u8, 128, 255]).collect();
|
||||
let bytes: Vec<u8> = (0..n).flat_map(|_| row.iter().copied()).collect();
|
||||
s.write_interleaved(r, &bytes);
|
||||
let p = FilterParams::GaussianBlur { radius };
|
||||
let area = output_area(&p, s.content_bounds(), r, None).intersect(&r);
|
||||
let t = std::time::Instant::now();
|
||||
let out = apply(&s, &p, area, r, None);
|
||||
let dt = t.elapsed();
|
||||
println!("gaussian radius {radius} on {n}x{n} RGBA8: {:.2?} ({} tiles)", dt, out.tile_count());
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
//! Blurs: Gaussian, box, motion, radial, surface.
|
||||
|
||||
use photocraft_geom::Rect;
|
||||
|
||||
use crate::image::{Edge, Image, premultiply, unpremultiply};
|
||||
use crate::{Ctx, RadialMethod};
|
||||
|
||||
/// Normalized Gaussian kernel with standard deviation `sigma` (radius 3σ).
|
||||
pub(crate) fn gaussian_kernel(sigma: f32) -> Vec<f32> {
|
||||
if sigma < 0.05 {
|
||||
return vec![1.0];
|
||||
}
|
||||
let r = (sigma * 3.0).ceil() as i32;
|
||||
let k: Vec<f32> = (-r..=r).map(|i| (-(i * i) as f32 / (2.0 * sigma * sigma)).exp()).collect();
|
||||
let s: f32 = k.iter().sum();
|
||||
k.into_iter().map(|v| v / s).collect()
|
||||
}
|
||||
|
||||
/// Separable convolution of `src` (premultiplied internally) for `out`.
|
||||
/// `src` must cover `out` grown by the kernel radii.
|
||||
pub(crate) fn conv_sep(src: &Image, out: Rect, kx: &[f32], ky: &[f32], alpha: bool) -> Vec<f32> {
|
||||
let n = src.ch;
|
||||
let (rx, ry) = ((kx.len() / 2) as i32, (ky.len() / 2) as i32);
|
||||
let (ow, oh) = (out.width() as usize, out.height() as usize);
|
||||
let th = oh + 2 * ry as usize;
|
||||
// Premultiplied copy of the needed source window.
|
||||
let win = Rect::new(out.x0 - rx, out.y0 - ry, out.x1 + rx, out.y1 + ry);
|
||||
let ww = win.width() as usize;
|
||||
let mut p = vec![0.0f32; ww * win.height() as usize * n];
|
||||
for y in win.y0..win.y1 {
|
||||
for x in win.x0..win.x1 {
|
||||
let o = ((y - win.y0) as usize * ww + (x - win.x0) as usize) * n;
|
||||
for c in 0..n {
|
||||
p[o + c] = src.get(x, y, c);
|
||||
}
|
||||
}
|
||||
}
|
||||
premultiply(&mut p, n, alpha);
|
||||
// Horizontal pass: (ow × th).
|
||||
let mut tmp = vec![0.0f32; ow * th * n];
|
||||
for ty in 0..th {
|
||||
let row = &p[ty * ww * n..(ty + 1) * ww * n];
|
||||
let dst = &mut tmp[ty * ow * n..(ty + 1) * ow * n];
|
||||
for ox in 0..ow {
|
||||
let d = &mut dst[ox * n..(ox + 1) * n];
|
||||
for (i, kv) in kx.iter().enumerate() {
|
||||
let s = &row[(ox + i) * n..(ox + i + 1) * n];
|
||||
for c in 0..n {
|
||||
d[c] += s[c] * kv;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Vertical pass.
|
||||
let mut res = vec![0.0f32; ow * oh * n];
|
||||
for oy in 0..oh {
|
||||
let d = &mut res[oy * ow * n..(oy + 1) * ow * n];
|
||||
for (i, kv) in ky.iter().enumerate() {
|
||||
let s = &tmp[(oy + i) * ow * n..(oy + i + 1) * ow * n];
|
||||
for (dv, sv) in d.iter_mut().zip(s) {
|
||||
*dv += sv * kv;
|
||||
}
|
||||
}
|
||||
}
|
||||
unpremultiply(&mut res, n, alpha);
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn gaussian(src: &Image, out: Rect, ctx: &Ctx, radius: f32) -> Vec<f32> {
|
||||
let k = gaussian_kernel(radius);
|
||||
conv_sep(src, out, &k, &k, ctx.alpha)
|
||||
}
|
||||
|
||||
pub(crate) fn boxed(src: &Image, out: Rect, ctx: &Ctx, radius: f32) -> Vec<f32> {
|
||||
let r = radius.max(0.0).round() as usize;
|
||||
let k = vec![1.0 / (2 * r + 1) as f32; 2 * r + 1];
|
||||
conv_sep(src, out, &k, &k, ctx.alpha)
|
||||
}
|
||||
|
||||
fn average_samples(src: &Image, out: Rect, ctx: &Ctx, mut offsets: impl FnMut(f32, f32, &mut Vec<(f32, f32)>)) -> Vec<f32> {
|
||||
let n = src.ch;
|
||||
let mut res = Vec::with_capacity(out.width() as usize * out.height() as usize * n);
|
||||
let mut pts = Vec::new();
|
||||
let mut tmp = vec![0.0f32; n];
|
||||
let mut acc = vec![0.0f32; n];
|
||||
for y in out.y0..out.y1 {
|
||||
for x in out.x0..out.x1 {
|
||||
let (cx, cy) = (x as f32 + 0.5, y as f32 + 0.5);
|
||||
pts.clear();
|
||||
offsets(cx, cy, &mut pts);
|
||||
for a in acc.iter_mut() {
|
||||
*a = 0.0;
|
||||
}
|
||||
for &(sx, sy) in &pts {
|
||||
src.sample(sx, sy, Edge::Transparent, src.rect, ctx.alpha, &mut tmp);
|
||||
// Accumulate premultiplied.
|
||||
let a = if ctx.alpha { tmp[n - 1] } else { 1.0 };
|
||||
for c in 0..n {
|
||||
acc[c] += if ctx.alpha && c < n - 1 { tmp[c] * a } else { tmp[c] };
|
||||
}
|
||||
}
|
||||
let k = 1.0 / pts.len().max(1) as f32;
|
||||
for a in acc.iter_mut() {
|
||||
*a *= k;
|
||||
}
|
||||
if ctx.alpha {
|
||||
let a = acc[n - 1];
|
||||
for v in acc.iter_mut().take(n - 1) {
|
||||
*v = if a > 1e-7 { *v / a } else { 0.0 };
|
||||
}
|
||||
}
|
||||
res.extend_from_slice(&acc);
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
pub(crate) fn motion(src: &Image, out: Rect, ctx: &Ctx, angle: f32, distance: f32) -> Vec<f32> {
|
||||
let d = distance.abs();
|
||||
if d < 0.5 {
|
||||
return src.crop(out);
|
||||
}
|
||||
let (s, c) = angle.to_radians().sin_cos();
|
||||
let steps = d.ceil() as i32;
|
||||
average_samples(src, out, ctx, |x, y, pts| {
|
||||
for i in 0..=steps {
|
||||
let t = i as f32 / steps as f32 - 0.5;
|
||||
pts.push((x + c * d * t, y - s * d * t));
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn radial(src: &Image, out: Rect, ctx: &Ctx, amount: f32, method: RadialMethod, center: (f32, f32)) -> Vec<f32> {
|
||||
let b = ctx.bounds;
|
||||
let (cx, cy) = (b.x0 as f32 + b.width() as f32 * center.0, b.y0 as f32 + b.height() as f32 * center.1);
|
||||
let amount = amount.clamp(0.0, 100.0);
|
||||
average_samples(src, out, ctx, |x, y, pts| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
match method {
|
||||
RadialMethod::Spin => {
|
||||
// Arc of `amount` degrees centred on the pixel.
|
||||
let arc = amount.to_radians();
|
||||
let n = ((arc * r).ceil() as i32).clamp(1, 64);
|
||||
for i in 0..=n {
|
||||
let t = (i as f32 / n as f32 - 0.5) * arc;
|
||||
let (s, c) = t.sin_cos();
|
||||
pts.push((cx + dx * c - dy * s, cy + dx * s + dy * c));
|
||||
}
|
||||
}
|
||||
RadialMethod::Zoom => {
|
||||
// Samples along the ray, up to amount/2 % closer to the centre.
|
||||
let span = amount / 200.0;
|
||||
let n = ((span * r).ceil() as i32).clamp(1, 64);
|
||||
for i in 0..=n {
|
||||
let k = 1.0 - span * i as f32 / n as f32;
|
||||
pts.push((cx + dx * k, cy + dy * k));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn surface(src: &Image, out: Rect, ctx: &Ctx, radius: f32, threshold: f32) -> Vec<f32> {
|
||||
let n = src.ch;
|
||||
let cc = if ctx.alpha { n - 1 } else { n };
|
||||
let r = radius.max(0.0).round() as i32;
|
||||
let t = (threshold.max(1.0) / 255.0) * 2.5;
|
||||
let mut res = Vec::with_capacity(out.width() as usize * out.height() as usize * n);
|
||||
for y in out.y0..out.y1 {
|
||||
for x in out.x0..out.x1 {
|
||||
let p = src.px(x, y);
|
||||
for c in 0..cc {
|
||||
let v0 = p[c];
|
||||
let (mut acc, mut wsum) = (0.0, 0.0);
|
||||
for yy in y - r..=y + r {
|
||||
for xx in x - r..=x + r {
|
||||
if ctx.alpha && src.get(xx, yy, n - 1) <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let v = src.get(xx, yy, c);
|
||||
let w = (1.0 - (v - v0).abs() / t).max(0.0);
|
||||
acc += v * w;
|
||||
wsum += w;
|
||||
}
|
||||
}
|
||||
res.push(if wsum > 0.0 { acc / wsum } else { v0 });
|
||||
}
|
||||
if ctx.alpha {
|
||||
res.push(p[n - 1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
//! Distortions (inverse mapping + bilinear resampling): Twirl, Pinch,
|
||||
//! Spherize, Wave, Ripple, Polar Coordinates. All are centred on the
|
||||
//! reference bounds.
|
||||
|
||||
use photocraft_geom::Rect;
|
||||
|
||||
use crate::image::{Edge, Image};
|
||||
use crate::noise::hash01;
|
||||
use crate::other::edge_of;
|
||||
use crate::{Ctx, PolarMode, RippleSize, SpherizeMode, UndefinedAreas, WaveType};
|
||||
|
||||
fn remap(src: &Image, out: Rect, ctx: &Ctx, edge: Edge, f: impl Fn(f32, f32) -> (f32, f32)) -> Vec<f32> {
|
||||
let n = src.ch;
|
||||
let mut res = Vec::with_capacity(out.width() as usize * out.height() as usize * n);
|
||||
let mut tmp = vec![0.0f32; n];
|
||||
for y in out.y0..out.y1 {
|
||||
for x in out.x0..out.x1 {
|
||||
let (sx, sy) = f(x as f32 + 0.5, y as f32 + 0.5);
|
||||
src.sample(sx, sy, edge, ctx.bounds, ctx.alpha, &mut tmp);
|
||||
res.extend_from_slice(&tmp);
|
||||
}
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
fn centre(b: Rect) -> (f32, f32, f32) {
|
||||
let (w, h) = (b.width() as f32, b.height() as f32);
|
||||
(b.x0 as f32 + w / 2.0, b.y0 as f32 + h / 2.0, w.min(h) / 2.0)
|
||||
}
|
||||
|
||||
pub(crate) fn twirl(src: &Image, out: Rect, ctx: &Ctx, angle: f32) -> Vec<f32> {
|
||||
let (cx, cy, rr) = centre(ctx.bounds);
|
||||
let a = angle.to_radians();
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r >= rr || rr <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let t = a * (1.0 - r / rr);
|
||||
let (s, c) = t.sin_cos();
|
||||
(cx + dx * c - dy * s, cy + dx * s + dy * c)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn pinch(src: &Image, out: Rect, ctx: &Ctx, amount: f32) -> Vec<f32> {
|
||||
let (cx, cy, rr) = centre(ctx.bounds);
|
||||
let a = (amount / 100.0).clamp(-1.0, 1.0);
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r >= rr || r <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let rs = rr * (r / rr).powf(1.0 - a * 0.5);
|
||||
(cx + dx * rs / r, cy + dy * rs / r)
|
||||
})
|
||||
}
|
||||
|
||||
fn sphere_1d(rn: f32, a: f32) -> f32 {
|
||||
// rn in [0,1): positive amount magnifies the centre (samples closer in).
|
||||
let target = if a >= 0.0 { rn.asin() * 2.0 / std::f32::consts::PI } else { (rn * std::f32::consts::FRAC_PI_2).sin() };
|
||||
rn + a.abs() * (target - rn)
|
||||
}
|
||||
|
||||
pub(crate) fn spherize(src: &Image, out: Rect, ctx: &Ctx, amount: f32, mode: SpherizeMode) -> Vec<f32> {
|
||||
let b = ctx.bounds;
|
||||
let (cx, cy, rr) = centre(b);
|
||||
let (hx, hy) = (b.width() as f32 / 2.0, b.height() as f32 / 2.0);
|
||||
let a = (amount / 100.0).clamp(-1.0, 1.0);
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
match mode {
|
||||
SpherizeMode::Normal => {
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
if r >= rr || r <= 0.0 {
|
||||
return (x, y);
|
||||
}
|
||||
let rs = rr * sphere_1d(r / rr, a);
|
||||
(cx + dx * rs / r, cy + dy * rs / r)
|
||||
}
|
||||
SpherizeMode::HorizontalOnly => {
|
||||
if dx.abs() >= hx {
|
||||
return (x, y);
|
||||
}
|
||||
(cx + dx.signum() * hx * sphere_1d(dx.abs() / hx, a), y)
|
||||
}
|
||||
SpherizeMode::VerticalOnly => {
|
||||
if dy.abs() >= hy {
|
||||
return (x, y);
|
||||
}
|
||||
(x, cy + dy.signum() * hy * sphere_1d(dy.abs() / hy, a))
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Wave generator settings.
|
||||
pub(crate) struct WaveSpec {
|
||||
pub generators: u32,
|
||||
pub wavelength: (f32, f32),
|
||||
pub amplitude: (f32, f32),
|
||||
pub wave_type: WaveType,
|
||||
pub undefined: UndefinedAreas,
|
||||
pub seed: u32,
|
||||
}
|
||||
|
||||
fn shape(t: WaveType, phase: f32) -> f32 {
|
||||
let p = phase.rem_euclid(std::f32::consts::TAU) / std::f32::consts::TAU;
|
||||
match t {
|
||||
WaveType::Sine => (phase).sin(),
|
||||
WaveType::Triangle => 1.0 - 4.0 * (p - 0.5).abs(),
|
||||
WaveType::Square => {
|
||||
if p < 0.5 {
|
||||
1.0
|
||||
} else {
|
||||
-1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn wave(src: &Image, out: Rect, ctx: &Ctx, w: WaveSpec) -> Vec<f32> {
|
||||
let n = w.generators.clamp(1, 64);
|
||||
let lerp = |(a, b): (f32, f32), t: f32| a + (b - a).max(0.0) * t;
|
||||
let gens: Vec<(f32, f32, f32, f32)> = (0..n)
|
||||
.map(|i| {
|
||||
let r = |k: u32| hash01(i as i32, k as i32, 7, w.seed);
|
||||
(lerp(w.wavelength, r(1)).max(1.0), lerp(w.amplitude, r(2)), r(3) * std::f32::consts::TAU, r(4) * std::f32::consts::TAU)
|
||||
})
|
||||
.collect();
|
||||
let b = ctx.bounds;
|
||||
remap(src, out, ctx, edge_of(w.undefined), |x, y| {
|
||||
let (lx, ly) = (x - b.x0 as f32, y - b.y0 as f32);
|
||||
let (mut dx, mut dy) = (0.0, 0.0);
|
||||
for &(len, amp, p1, p2) in &gens {
|
||||
dx += amp * shape(w.wave_type, std::f32::consts::TAU * ly / len + p1);
|
||||
dy += amp * shape(w.wave_type, std::f32::consts::TAU * lx / len + p2);
|
||||
}
|
||||
(x + dx / n as f32, y + dy / n as f32)
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn ripple(src: &Image, out: Rect, ctx: &Ctx, amount: f32, size: RippleSize) -> Vec<f32> {
|
||||
let len = match size {
|
||||
RippleSize::Small => 8.0,
|
||||
RippleSize::Medium => 16.0,
|
||||
RippleSize::Large => 32.0,
|
||||
};
|
||||
let amp = amount / 100.0 * len / 8.0;
|
||||
let b = ctx.bounds;
|
||||
remap(src, out, ctx, Edge::Repeat, |x, y| {
|
||||
let (lx, ly) = (x - b.x0 as f32, y - b.y0 as f32);
|
||||
(x + amp * (std::f32::consts::TAU * ly / len).sin(), y + amp * (std::f32::consts::TAU * lx / len).sin())
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn polar(src: &Image, out: Rect, ctx: &Ctx, mode: PolarMode) -> Vec<f32> {
|
||||
let b = ctx.bounds;
|
||||
let (w, h) = (b.width().max(1) as f32, b.height().max(1) as f32);
|
||||
let (cx, cy) = (b.x0 as f32 + w / 2.0, b.y0 as f32 + h / 2.0);
|
||||
let rmax = w.min(h) / 2.0;
|
||||
remap(src, out, ctx, Edge::Transparent, |x, y| match mode {
|
||||
PolarMode::RectangularToPolar => {
|
||||
// Output is polar: angle from 12 o'clock clockwise → source x,
|
||||
// radius → source y.
|
||||
let (dx, dy) = (x - cx, y - cy);
|
||||
let theta = dx.atan2(-dy).rem_euclid(std::f32::consts::TAU);
|
||||
let r = (dx * dx + dy * dy).sqrt();
|
||||
(b.x0 as f32 + theta / std::f32::consts::TAU * w, b.y0 as f32 + r / rmax * h)
|
||||
}
|
||||
PolarMode::PolarToRectangular => {
|
||||
let theta = (x - b.x0 as f32) / w * std::f32::consts::TAU;
|
||||
let r = (y - b.y0 as f32) / h * rmax;
|
||||
(cx + r * theta.sin(), cy - r * theta.cos())
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Interleaved `f32` image regions and sampling helpers.
|
||||
|
||||
use photocraft_geom::Rect;
|
||||
use photocraft_raster::Surface;
|
||||
|
||||
/// How samples outside an image are read.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Edge {
|
||||
/// Zero (transparent).
|
||||
Transparent,
|
||||
/// Clamp to the nearest edge pixel.
|
||||
Repeat,
|
||||
/// Wrap around.
|
||||
Wrap,
|
||||
}
|
||||
|
||||
/// A rectangle of interleaved, normalized samples.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct Image {
|
||||
pub rect: Rect,
|
||||
/// Channels per pixel (alpha last when the surface has alpha).
|
||||
pub ch: usize,
|
||||
pub data: Vec<f32>,
|
||||
}
|
||||
|
||||
impl Image {
|
||||
/// Reads `rect` from a surface.
|
||||
pub fn read(s: &Surface, rect: Rect) -> Self {
|
||||
Image { rect, ch: s.channels(), data: s.read_region(rect) }
|
||||
}
|
||||
|
||||
/// New zeroed image.
|
||||
pub fn new(rect: Rect, ch: usize) -> Self {
|
||||
Image { rect, ch, data: vec![0.0; rect.width() as usize * rect.height() as usize * ch] }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn w(&self) -> i32 {
|
||||
self.rect.width() as i32
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn index(&self, x: i32, y: i32) -> usize {
|
||||
((y - self.rect.y0) as usize * self.rect.width() as usize + (x - self.rect.x0) as usize) * self.ch
|
||||
}
|
||||
|
||||
/// Sample at integer coordinates; 0 outside.
|
||||
#[inline]
|
||||
pub fn get(&self, x: i32, y: i32, c: usize) -> f32 {
|
||||
if self.rect.contains(x, y) { self.data[self.index(x, y) + c] } else { 0.0 }
|
||||
}
|
||||
|
||||
/// Pixel slice at `(x, y)` (must be inside).
|
||||
#[inline]
|
||||
pub fn px(&self, x: i32, y: i32) -> &[f32] {
|
||||
let i = self.index(x, y);
|
||||
&self.data[i..i + self.ch]
|
||||
}
|
||||
|
||||
/// Sample with an edge policy relative to `area`.
|
||||
#[inline]
|
||||
pub fn get_edge(&self, x: i32, y: i32, c: usize, edge: Edge, area: Rect) -> f32 {
|
||||
if area.contains(x, y) {
|
||||
return self.get(x, y, c);
|
||||
}
|
||||
match edge {
|
||||
Edge::Transparent => 0.0,
|
||||
Edge::Repeat => self.get(x.clamp(area.x0, area.x1 - 1), y.clamp(area.y0, area.y1 - 1), c),
|
||||
Edge::Wrap => {
|
||||
let (w, h) = (area.width().max(1) as i32, area.height().max(1) as i32);
|
||||
self.get(area.x0 + (x - area.x0).rem_euclid(w), area.y0 + (y - area.y0).rem_euclid(h), c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Bilinear sample of the premultiplied pixel at `(x, y)` (pixel centres
|
||||
/// at `.5`), written to `out` (straight colour, alpha last).
|
||||
pub fn sample(&self, x: f32, y: f32, edge: Edge, area: Rect, alpha: bool, out: &mut [f32]) {
|
||||
let (fx, fy) = (x - 0.5, y - 0.5);
|
||||
let (x0, y0) = (fx.floor(), fy.floor());
|
||||
let (ax, ay) = (fx - x0, fy - y0);
|
||||
let (x0, y0) = (x0 as i32, y0 as i32);
|
||||
let n = self.ch;
|
||||
for v in out.iter_mut() {
|
||||
*v = 0.0;
|
||||
}
|
||||
let mut acc_a = 0.0;
|
||||
for (dx, dy, wgt) in [(0, 0, (1.0 - ax) * (1.0 - ay)), (1, 0, ax * (1.0 - ay)), (0, 1, (1.0 - ax) * ay), (1, 1, ax * ay)] {
|
||||
if wgt <= 0.0 {
|
||||
continue;
|
||||
}
|
||||
let a = if alpha { self.get_edge(x0 + dx, y0 + dy, n - 1, edge, area) } else { 1.0 };
|
||||
for (c, o) in out.iter_mut().enumerate().take(if alpha { n - 1 } else { n }) {
|
||||
*o += self.get_edge(x0 + dx, y0 + dy, c, edge, area) * a * wgt;
|
||||
}
|
||||
acc_a += a * wgt;
|
||||
}
|
||||
if alpha {
|
||||
if acc_a > 0.0 {
|
||||
for o in out.iter_mut().take(n - 1) {
|
||||
*o /= acc_a;
|
||||
}
|
||||
}
|
||||
out[n - 1] = acc_a;
|
||||
}
|
||||
}
|
||||
|
||||
/// Copy of the samples inside `out` (for identity passes).
|
||||
pub fn crop(&self, out: Rect) -> Vec<f32> {
|
||||
let mut v = Vec::with_capacity(out.width() as usize * out.height() as usize * self.ch);
|
||||
for y in out.y0..out.y1 {
|
||||
for x in out.x0..out.x1 {
|
||||
for c in 0..self.ch {
|
||||
v.push(self.get(x, y, c));
|
||||
}
|
||||
}
|
||||
}
|
||||
v
|
||||
}
|
||||
}
|
||||
|
||||
/// Premultiplies colour channels by alpha (in place). No-op without alpha.
|
||||
pub fn premultiply(data: &mut [f32], ch: usize, alpha: bool) {
|
||||
if !alpha {
|
||||
return;
|
||||
}
|
||||
for px in data.chunks_exact_mut(ch) {
|
||||
let a = px[ch - 1];
|
||||
for v in px.iter_mut().take(ch - 1) {
|
||||
*v *= a;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Inverse of [`premultiply`].
|
||||
pub fn unpremultiply(data: &mut [f32], ch: usize, alpha: bool) {
|
||||
if !alpha {
|
||||
return;
|
||||
}
|
||||
for px in data.chunks_exact_mut(ch) {
|
||||
let a = px[ch - 1];
|
||||
for v in px.iter_mut().take(ch - 1) {
|
||||
*v = if a > 1e-7 { *v / a } else { 0.0 };
|
||||
}
|
||||
}
|
||||
}
|
||||