Skip to content

Commit

Permalink
Pixel Math 2 using CFFI
Browse files Browse the repository at this point in the history
Might even be possible to port Filmic Chroma to it. It'll need to FFI
directly still but one less build to maintain.
  • Loading branch information
Beinsezii committed Jan 1, 2021
1 parent 5766b21 commit e038995
Show file tree
Hide file tree
Showing 6 changed files with 323 additions and 3 deletions.
4 changes: 2 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
__pycache__/
filmic_chroma.dll
filmic_chroma.so
*.dll
*.so

# Stored in separate git project as it's a generic Gtk 3 library useful (for me) outside of GIMP
# The plugins all import the bszgw.py in the git root, so all changes apply
Expand Down
7 changes: 6 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,11 @@ Enter custom Python algorithms for pixel math.

<img width=300 src="./bsz-pixel-math/during.png" />

### Pixel Math 2
Enter basic operations to perform pixel math. More limited, but processes well over 20x faster than Pixel Math in the worst scenarios.

<img width=300 src="./bsz-pixel-math-2/during.png" />


## bsz_gimp_lib
Shared library for plugins. Notably contains a *complete plugin auto-builder*. Similar to the old python-fu, but (imo) significantly more customizable at a mild complexity cost. Features include
Expand All @@ -97,4 +102,4 @@ Other non-plugin-builder bits include:
- Premade dictionary of Gegl compositors
- Ripped from the gegl site's html.
- Only includes operations that use pads input, aux, output.
- PDB quick function. WIP, only used for gimp-message atm.
- PDB quick function. WIP, many datatypes not supported.
145 changes: 145 additions & 0 deletions bsz-pixel-math-2/bsz-pixel-math-2.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Gegl Operation references:
http://www.gegl.org/operations/
https://gitlab.gnome.org/GNOME/gegl/-/tree/master/operations
If on Linux:
$ gegl --list-all
$ gegl --info "operation-name"
Also build the .gir files using g-ir-doc-tool for additional insight.
If the docs don't have a description on something like class methods,
run python's help() on it to view it in the terminal.
"""

# Uncomment as needed.
# I don't actually know if it's faster to not import some of the gi repo stuff
# since it probably gets imported later anyway ...right?
import gi
gi.require_version('Gimp', '3.0')
from gi.repository import Gimp
gi.require_version('Gegl', '0.4')
from gi.repository import Gegl
# from gi.repository import GObject
# from gi.repository import GLib
# from gi.repository import Gio
import sys
import os.path
sys.path.append(os.path.dirname(os.path.realpath(__file__)) + '/../')
from bsz_gimp_lib import PlugIn, ParamCombo, ParamString

import ctypes
from sys import platform
EXTENSIONS = {"win32": ".dll", "linux": ".so"}
PM2 = ctypes.CDLL(
os.path.dirname(os.path.realpath(__file__)) +
"/pixel_math_2" + EXTENSIONS.get(platform)
).pixel_math_2
PM2.argtypes = [ctypes.c_char_p, ctypes.c_char_p, ctypes.c_uint]


FORMATS = {
"RGBA": "RGBA double",
"HSLA": "HSLA double",
"LABA": "CIE Lab alpha double",
"LCHA": "CIE LCH(ab) alpha double",
}


# Main function.
def pixel_math(image, drawable, babl_format, code):
# {{{
# Fairly certain mask_intersect() is the current selection mask
intersect, x, y, width, height = drawable.mask_intersect()
if intersect:
# start Gegl
Gegl.init(None)
# fetch main buffer
buff = drawable.get_buffer()

# fetch shadow aka "temp" buffer
shadow = drawable.get_shadow_buffer()

# create working rectangle area using mask intersect.
rect = Gegl.Rectangle.new(x, y, width, height)

if babl_format == "RGBA double":
channels = "rgba"
elif babl_format == "HSLA double":
channels = "hsla"
elif babl_format == "CIE Lab alpha double":
channels = "laba"
elif babl_format == "CIE LCH(ab) alpha double":
channels = "lcha"
else:
raise ValueError("Invalid/unsupported BABL format")

code = code.casefold()

for num, c in enumerate(channels):
code = code.replace(f"{c} ", f"c{num+1} ")
code = code.replace(f"{c}\n", f"c{num+1}\n")
# since everything is whitespace separated in rust
# you can just search for whitespace to avoid
# double-replacing 'c'

pixels = buff.get(rect, 1.0, babl_format,
Gegl.AbyssPolicy.CLAMP)

PM2(code.encode('UTF-8'), pixels, len(pixels))

shadow.set(rect, babl_format, bytes(pixels))

# Flush shadow buffer and combine it with main drawable
shadow.flush()
drawable.merge_shadow(True)

# Update everything.
drawable.update(x, y, width, height)
Gimp.displays_flush()
# }}}


# create the plugin from bsz_gimp_lib
plugin = PlugIn(
"Pixel Math 2", # name
pixel_math, # function
ParamCombo('Format', FORMATS, "HSLA double", "Pixel format"),

ParamString("Operations",
"v1 = L\n"
"L = 1\n"
"L - v1",
"See description for code documentation",
ui_multiline=True,
ui_min_width=600, ui_min_height=200),

description="""\
Pixel math. Code format is {channel} {operator} {value}
So c1 + 0.5 will add 0.5 to the first channel.
Available channels:
c1, c2, c3, c4 (always alpha). These are mapped to RGBA, HSLA, etc.
You may also use the channel letters themselves, such as
r, g, b, a
Supported operators are
= or == : set/assign
+ or += : add
- or -= : subtract
* or *= : multiply
/ or /= : divide
In addition to channels, you can store temporary values in variables.
Available variables:
v1, v2, v3...v9
""",
images="RGB*, GRAY*",
)

# register the plugin's Procedure class with gimp
Gimp.main(plugin.Procedure.__gtype__, sys.argv)
7 changes: 7 additions & 0 deletions bsz-pixel-math-2/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
cd "${0%/*}"
rustc --crate-type cdylib -O pixel-math-2.rs
mv libpixel_math_2.so pixel_math_2.so
strip pixel_math_2.so
rustc --crate-type cdylib -O --target x86_64-pc-windows-gnu pixel-math-2.rs
rm libpixel_math_2.dll.a
strip pixel_math_2.dll
Binary file added bsz-pixel-math-2/during.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
163 changes: 163 additions & 0 deletions bsz-pixel-math-2/pixel-math-2.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
use std::convert::TryInto;
use std::os::raw;


#[derive(Clone, Copy)]
enum Op {
Add,
Sub,
Mul,
Div,
Set,
}


#[derive(Clone, Copy)]
enum Obj {
Chan(usize),
Var(usize),
Num(f64),
}


#[derive(Clone, Copy)]
struct Operation {
target: Obj,
operation: Op,
value: Obj,
}


fn parse_ops(ops: &str) -> Vec<Operation> {
let mut line = 0;
let mut result = Vec::<Operation>::new();
for op in ops.trim().to_ascii_lowercase().split('\n') {
line += 1;
let items = op.split_ascii_whitespace().collect::<Vec<&str>>();
if items.len() != 3 {
println!("Invalid number of args on operation line {}", line);
continue
}

result.push( Operation{
target: match items[0] {
// don't hate I made these with a vim macro
"c1" => Obj::Chan(0),
"c2" => Obj::Chan(1),
"c3" => Obj::Chan(2),
"c4" => Obj::Chan(3),
"v1" => Obj::Var(0),
"v2" => Obj::Var(1),
"v3" => Obj::Var(2),
"v4" => Obj::Var(3),
"v5" => Obj::Var(4),
"v6" => Obj::Var(5),
"v7" => Obj::Var(6),
"v8" => Obj::Var(7),
"v9" => Obj::Var(8),
_ => {
println!("Invalid target on operation line {}", line);
continue
},
},

operation: match items[1] {
"+=" | "+" => Op::Add,
"-=" | "-" => Op::Sub,
"*=" | "*" => Op::Mul,
"/=" | "/" => Op::Div,
"==" | "=" => Op::Set,
_ => {
println!("Invalid math operator on operation line {}", line);
continue
},
},

value: match items[2] {
"c1" => Obj::Chan(0),
"c2" => Obj::Chan(1),
"c3" => Obj::Chan(2),
"c4" => Obj::Chan(3),
"v1" => Obj::Var(0),
"v2" => Obj::Var(1),
"v3" => Obj::Var(2),
"v4" => Obj::Var(3),
"v5" => Obj::Var(4),
"v6" => Obj::Var(5),
"v7" => Obj::Var(6),
"v8" => Obj::Var(7),
"v9" => Obj::Var(8),
s => {
match s.parse::<f64>() {
Ok(n) => Obj::Num(n),
Err(_) => {
println!("Invalid value on operation line {}", line);
continue
}
}
},
}
});

}

result
}


#[no_mangle]
pub extern "C" fn pixel_math_2(
code: *const raw::c_char,
pixels: *mut raw::c_char,
len: usize) {

let code = unsafe {
assert!(!code.is_null());
std::ffi::CStr::from_ptr(code).to_str().expect("Invalid code string")
};
let pixels = unsafe {
assert!(!pixels.is_null());
std::slice::from_raw_parts_mut(pixels.cast::<u8>(), len)
};

let ops = parse_ops(code);

for chunk in pixels.chunks_exact_mut(32) {
let mut c: [f64; 4] = [
f64::from_le_bytes(chunk[0..8].try_into().expect("bytefail c1")),
f64::from_le_bytes(chunk[8..16].try_into().expect("bytefail c2")),
f64::from_le_bytes(chunk[16..24].try_into().expect("bytefail c3")),
f64::from_le_bytes(chunk[24..32].try_into().expect("bytefail c4")),
];
let mut v: [f64; 9] = [0.0; 9];

for op in ops.iter() {
let val: f64 = match op.value {
Obj::Chan(i) => c[i],
Obj::Var(i) => v[i],
Obj::Num(n) => n,
};
let tar: &mut f64 = match op.target {
Obj::Chan(i) => &mut c[i],
Obj::Var(i) => &mut v[i],
Obj::Num(_) => panic!("This shouldn't be reachable"),
};
match op.operation {
Op::Add => *tar += val,
Op::Sub => *tar -= val,
Op::Mul => *tar *= val,
Op::Div => *tar /= val,
Op::Set => *tar = val,
};
}

let c1b = c[0].to_le_bytes();
let c2b = c[1].to_le_bytes();
let c3b = c[2].to_le_bytes();
let c4b = c[3].to_le_bytes();
for x in 0..8 {chunk[x] = c1b[x]};
for x in 0..8 {chunk[x+8] = c2b[x]};
for x in 0..8 {chunk[x+16] = c3b[x]};
for x in 0..8 {chunk[x+24] = c4b[x]};
};
}

0 comments on commit e038995

Please sign in to comment.