OmaPhoto 1.0.0: the Linux port of Compositor
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s

A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
雷电芽衣andClaude committed 2026-09-26 00:44:52 -04:00
commit c864304695
564 files changed
+100804

No files matched your search

+96
View File
@@ -0,0 +1,96 @@
#include "AdjustPixels.h"
#include <math.h>
void adjust_gradient_map(uint8_t *rgba, size_t width, size_t height, size_t stride, const uint8_t *table) {
for (size_t y = 0; y < height; y++) {
uint8_t *p = rgba + y * stride;
for (size_t x = 0; x < width; x++, p += 4) {
unsigned a = p[3];
if (a == 0) continue;
unsigned r = p[0], g = p[1], b = p[2];
if (a < 255) {
r = (r * 255u + a / 2) / a;
g = (g * 255u + a / 2) / a;
b = (b * 255u + a / 2) / a;
if (r > 255) r = 255;
if (g > 255) g = 255;
if (b > 255) b = 255;
}
unsigned level = (2126u * r + 7152u * g + 722u * b + 5000u) / 10000u;
const uint8_t *color = table + (level > 255 ? 255 : level) * 3;
p[0] = (uint8_t)((color[0] * a + 127u) / 255u);
p[1] = (uint8_t)((color[1] * a + 127u) / 255u);
p[2] = (uint8_t)((color[2] * a + 127u) / 255u);
}
}
}
static inline uint32_t mix32(uint32_t x) {
x ^= x >> 16;
x *= 0x7feb352dU;
x ^= x >> 15;
x *= 0x846ca68bU;
x ^= x >> 16;
return x;
}
// A value in −1…1 for an integer lattice point, fixed by the point and the seed. Two uniform halves
// summed give a triangular spread, closer to film grain than flat noise.
static inline float lattice(int64_t ix, int64_t iy, uint32_t seed) {
uint32_t h = mix32((uint32_t)ix * 0x9E3779B1U ^ mix32((uint32_t)iy * 0x85EBCA77U ^ seed));
return (float)(h & 0xFFFFU) / 65535.0f + (float)(h >> 16) / 65535.0f - 1.0f;
}
static inline float clamp255(float value) { return value < 0 ? 0 : value > 255 ? 255 : value; }
void adjust_grain(uint8_t *rgba, size_t width, size_t height, size_t stride, double amount, double size,
double roughness, uint32_t seed, double originX, double originY, double unitsPerPixel) {
if (!(amount > 0) || !(unitsPerPixel > 0)) return;
if (!(size > 0)) size = 1;
float strength = (float)(amount > 100 ? 1.0 : amount / 100.0) * 0.35f * 255.0f;
float rough = (float)(roughness < 0 ? 0.0 : roughness > 100 ? 1.0 : roughness / 100.0);
uint32_t fineSeed = mix32(seed ^ 0xA511E9B3U);
for (size_t y = 0; y < height; y++) {
double v = originY + ((double)y + 0.5) * unitsPerPixel;
double cellY = floor(v / size);
float ty = (float)(v / size - cellY);
ty = ty * ty * (3.0f - 2.0f * ty);
int64_t iy = (int64_t)cellY, fineY = (int64_t)floor(v);
uint8_t *p = rgba + y * stride;
for (size_t x = 0; x < width; x++, p += 4) {
unsigned a = p[3];
if (a == 0) continue;
double u = originX + ((double)x + 0.5) * unitsPerPixel;
double cellX = floor(u / size);
float tx = (float)(u / size - cellX);
tx = tx * tx * (3.0f - 2.0f * tx);
int64_t ix = (int64_t)cellX;
float n00 = lattice(ix, iy, seed), n10 = lattice(ix + 1, iy, seed);
float n01 = lattice(ix, iy + 1, seed), n11 = lattice(ix + 1, iy + 1, seed);
float top = n00 + (n10 - n00) * tx, bottom = n01 + (n11 - n01) * tx;
// Blending neighbors narrows the spread; scaling restores about the lattice's own.
float smooth = (top + (bottom - top) * ty) * 1.6f;
float fine = lattice((int64_t)floor(u), fineY, fineSeed);
float noise = smooth + (fine - smooth) * rough;
float unpremultiply = a == 255 ? 1.0f : 255.0f / (float)a;
float r = p[0] * unpremultiply, g = p[1] * unpremultiply, b = p[2] * unpremultiply;
float level = (0.2126f * r + 0.7152f * g + 0.0722f * b) / 255.0f;
if (level > 1) level = 1;
// Film grain shows most in the midtones.
float delta = noise * strength * (0.4f + 2.4f * level * (1.0f - level));
float coverage = (float)a / 255.0f;
p[0] = (uint8_t)(clamp255(r + delta) * coverage + 0.5f);
p[1] = (uint8_t)(clamp255(g + delta) * coverage + 0.5f);
p[2] = (uint8_t)(clamp255(b + delta) * coverage + 0.5f);
}
}
}
void rgba_clamp_premultiplied(uint8_t *rgba, size_t count) {
for (size_t i = 0; i < count; i++, rgba += 4) {
uint8_t a = rgba[3];
if (rgba[0] > a) rgba[0] = a;
if (rgba[1] > a) rgba[1] = a;
if (rgba[2] > a) rgba[2] = a;
}
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef AdjustPixels_h
#define AdjustPixels_h
#include <stdint.h>
#include <stddef.h>
// Gradient Map on premultiplied RGBA pixels (4 bytes per pixel, `stride` bytes per row): each pixel's
// luminance picks a color from `table` (256 × 3 straight sRGB bytes, darkest first). Alpha is kept and
// fully transparent pixels are left alone.
void adjust_gradient_map(uint8_t *rgba, size_t width, size_t height, size_t stride, const uint8_t *table);
// Film grain on premultiplied RGBA pixels: the same brightness change on all three channels, strongest
// in the midtones. `amount` is 0–100, `size` the grain's scale in document units, and `roughness`
// (0–100) mixes in per-unit noise. Pixel (x, y) sits at (originX + (x + 0.5) × unitsPerPixel,
// originY + (y + 0.5) × unitsPerPixel), and its grain depends only on that position and `seed`, so a
// piece of an image gets the same grain as that part of the whole.
void adjust_grain(uint8_t *rgba, size_t width, size_t height, size_t stride, double amount, double size,
double roughness, uint32_t seed, double originX, double originY, double unitsPerPixel);
// After resampling with a filter that rings (Lanczos), premultiplied RGBA colors can exceed their alpha;
// this clamps each channel back to its pixel's alpha. `count` is the number of pixels.
void rgba_clamp_premultiplied(uint8_t *rgba, size_t count);
#endif
+50
View File
@@ -0,0 +1,50 @@
#include "BrushPixels.h"
void brush_alpha_bounds(const uint8_t *bytes, size_t width, size_t height, size_t stride, size_t bounds[4]) {
size_t left = width, right = 0, top = height, bottom = 0;
for (size_t y = 0; y < height; ++y) {
const uint8_t *row = bytes + y * stride;
size_t first = 0;
while (first < width && row[first * 4 + 3] == 0) ++first;
if (first == width) continue;
size_t last = width;
while (last > first && row[(last - 1) * 4 + 3] == 0) --last;
if (first < left) left = first;
if (last > right) right = last;
if (y < top) top = y;
bottom = y + 1;
}
bounds[0] = right ? left : 0;
bounds[1] = right ? top : 0;
bounds[2] = right;
bounds[3] = bottom;
}
void layer_unpremultiply_opaque(uint8_t *rgba, size_t stride, size_t width, size_t height) {
for (size_t y = 0; y < height; ++y) {
uint8_t *p = rgba + y * stride;
for (size_t x = 0; x < width; ++x, p += 4) {
unsigned a = p[3];
for (int c = 0; c < 3; ++c) {
unsigned v = a ? (p[c] * 255u + a / 2) / a : 0;
p[c] = v > 255 ? 255 : v;
}
p[3] = 255;
}
}
}
void layer_restore_alpha(uint8_t *rgba, size_t stride, const uint8_t *alpha, size_t alphaStride, size_t width, size_t height) {
for (size_t y = 0; y < height; ++y) {
uint8_t *p = rgba + y * stride;
for (size_t x = 0; x < width; ++x, p += 4) {
unsigned a = alpha[y * alphaStride + x];
for (int c = 0; c < 3; ++c) p[c] = (p[c] * a + 127) / 255;
p[3] = a;
}
}
}
void layer_extract_alpha(const uint8_t *rgba, size_t rgbaStride, uint8_t *gray, size_t grayStride, size_t width, size_t height) {
for (size_t y = 0; y < height; ++y)
for (size_t x = 0; x < width; ++x)
gray[y * grayStride + x] = rgba[y * rgbaStride + x * 4 + 3];
}
+10
View File
@@ -0,0 +1,10 @@
#ifndef BrushPixels_h
#define BrushPixels_h
#include <stdint.h>
#include <stddef.h>
// Half-open bounds of nonzero alpha in premultiplied RGBA. Empty returns all zero.
void brush_alpha_bounds(const uint8_t *bytes, size_t width, size_t height, size_t stride, size_t bounds[4]);
void layer_extract_alpha(const uint8_t *rgba, size_t rgbaStride, uint8_t *gray, size_t grayStride, size_t width, size_t height);
void layer_unpremultiply_opaque(uint8_t *rgba, size_t stride, size_t width, size_t height);
void layer_restore_alpha(uint8_t *rgba, size_t stride, const uint8_t *alpha, size_t alphaStride, size_t width, size_t height);
#endif
+93
View File
@@ -0,0 +1,93 @@
#include "ContentFill.h"
#include <stdlib.h>
#include <string.h>
#include <float.h>
static uint32_t next_random(uint32_t *state) {
*state = *state * 1664525u + 1013904223u;
return *state;
}
static double match(const uint8_t *pixels, size_t stride, const uint8_t *known,
int w, int h, int p, int q, int radius) {
int px=p%w, py=p/w, qx=q%w, qy=q/w, count=0;
double sum=0;
for(int dy=-radius; dy<=radius; ++dy) for(int dx=-radius; dx<=radius; ++dx) {
int x=px+dx,y=py+dy,sx=qx+dx,sy=qy+dy;
if(x<0||y<0||x>=w||y>=h||sx<0||sy<0||sx>=w||sy>=h||!known[y*w+x]) continue;
const uint8_t *a=pixels+y*stride+x*4, *b=pixels+sy*stride+sx*4;
for(int c=0;c<4;++c) { int d=a[c]-b[c]; sum+=d*d; }
++count;
}
return count ? sum/count : DBL_MAX;
}
int content_fill(uint8_t *pixels, size_t stride, const uint8_t *mask, size_t ms, int w, int h) {
size_t n=(size_t)w*h;
uint8_t *known=calloc(n,1), *target=calloc(n,1), *valid=calloc(n,1), *queued=calloc(n,1);
int *donors=malloc(n*sizeof(int)), *queue=malloc(n*sizeof(int)), *chosen=malloc(n*sizeof(int));
if(!known||!target||!valid||!queued||!donors||!queue||!chosen) {
free(known);free(target);free(valid);free(queued);free(donors);free(queue);free(chosen);return -1;
}
int radius=(w>=5 && h>=5)?2:0;
size_t missing=0, donorCount=0, head=0,tail=0, scan=0;
// Selected pixels are filled. Unselected opaque pixels are the image to match and copy from; unselected
// transparent ones are neither — nothing to match against, and left as they are.
for(int y=0;y<h;++y) for(int x=0;x<w;++x) {
int p=y*w+x; target[p]=mask[y*ms+x]!=0; known[p]=!target[p] && pixels[y*stride+x*4+3]==255; chosen[p]=-1;
if(target[p]) ++missing;
}
if(!missing) { donorCount=1; goto done; }
for(int y=0;y<h;++y) for(int x=0;x<w;++x) {
int p=y*w+x;
if(!known[p]) continue;
int ok=1;
for(int dy=-radius;dy<=radius&&ok;++dy) for(int dx=-radius;dx<=radius;++dx) {
int sx=x+dx,sy=y+dy;
if(sx<0||sy<0||sx>=w||sy>=h||!known[sy*w+sx]) {ok=0;break;}
}
if(ok) { valid[p]=1;donors[donorCount++]=p; }
}
if(!donorCount) goto done;
for(int y=0;y<h;++y) for(int x=0;x<w;++x) {
int p=y*w+x;
if(target[p] && ((x&&known[p-1])||(x+1<w&&known[p+1])||(y&&known[p-w])||(y+1<h&&known[p+w]))) {
queue[tail++]=p;queued[p]=1;
}
}
uint32_t seed=0x6d2b79f5;
for(;;) {
while(head<tail) {
int p=queue[head++],x=p%w,y=p/w,best=-1;
double score=DBL_MAX;
int neighbors[4]={x?p-1:-1,x+1<w?p+1:-1,y?p-w:-1,y+1<h?p+w:-1};
// Propagate coherent source offsets, then refine with randomized patch search.
for(int k=0;k<28;++k) {
int q=-1;
if(k<4) {
int t=neighbors[k];
if(t>=0) q=(chosen[t]>=0?chosen[t]:t)+(p-t);
} else q=donors[next_random(&seed)%donorCount];
if(q<0||(size_t)q>=n||!valid[q]) continue;
double s=match(pixels,stride,known,w,h,p,q,radius);
if(best<0||s<score) {score=s;best=q;}
}
if(best<0) best=donors[0];
for(int r=64;r>=1;r/=2) {
int qx=best%w+(int)(next_random(&seed)%(2*r+1))-r;
int qy=best/w+(int)(next_random(&seed)%(2*r+1))-r;
if(qx<0||qy<0||qx>=w||qy>=h||!valid[qy*w+qx]) continue;
int q=qy*w+qx; double s=match(pixels,stride,known,w,h,p,q,radius);
if(s<score) {score=s;best=q;}
}
memcpy(pixels+y*stride+x*4,pixels+(best/w)*stride+(best%w)*4,4);
known[p]=1;chosen[p]=best;
for(int k=0;k<4;++k) { int q=neighbors[k]; if(q>=0&&target[q]&&!known[q]&&!queued[q]) {queued[q]=1;queue[tail++]=q;} }
}
// A selected area that only transparency touches starts from the best random donor, then spreads.
while(scan<n && (!target[scan]||known[scan])) ++scan;
if(scan>=n) break;
queue[tail++]=(int)scan; queued[scan]=1;
}
done:
free(known);free(target);free(valid);free(queued);free(donors);free(queue);free(chosen);
return donorCount ? 1 : 0;
}
+4
View File
@@ -0,0 +1,4 @@
#include <stddef.h>
#include <stdint.h>
// Returns 1 on success, 0 when no source patch exists, -1 on allocation failure.
int content_fill(uint8_t *rgba, size_t stride, const uint8_t *mask, size_t maskStride, int width, int height);
+259
View File
@@ -0,0 +1,259 @@
#include "HealPixels.h"
#include <math.h>
#include <stdlib.h>
#include <string.h>
enum { OUTSIDE = 0, RING = 1, HOLE = 2 };
void heal_coverage_bounds(const uint8_t *gray, size_t width, size_t height, size_t stride, long bounds[4]) {
long x0 = (long)width, y0 = (long)height, x1 = 0, y1 = 0;
for (size_t y = 0; y < height; ++y) {
const uint8_t *row = gray + y * stride;
for (size_t x = 0; x < width; ++x) {
if (!row[x]) continue;
if ((long)x < x0) x0 = (long)x;
if ((long)x + 1 > x1) x1 = (long)x + 1;
if ((long)y < y0) y0 = (long)y;
if ((long)y + 1 > y1) y1 = (long)y + 1;
}
}
if (x1 <= x0 || y1 <= y0) x0 = y0 = x1 = y1 = 0;
bounds[0] = x0; bounds[1] = y0; bounds[2] = x1; bounds[3] = y1;
}
static inline uint32_t heal_hash(uint32_t x) {
x ^= x >> 16; x *= 0x7feb352dU;
x ^= x >> 15; x *= 0x846ca68bU;
x ^= x >> 16;
return x;
}
static inline double heal_unit(uint32_t key) { return (double)(heal_hash(key) >> 8) / 16777216.0; }
// Mean squared difference between the ring around the spot and the ring around the patch
// offset by (dx, dy). Infinite when the patch would overlap the spot or leave the image.
static double heal_score(const uint8_t *rgba, size_t stride, const uint8_t *role, long wx0, long wy0,
long ww, long wh, long dx, long dy, long W, long H) {
if (labs(dx) < ww && labs(dy) < wh) return INFINITY;
if (wx0 + dx < 0 || wy0 + dy < 0 || wx0 + ww + dx > W || wy0 + wh + dy > H) return INFINITY;
double sum = 0;
long n = 0;
for (long y = 0; y < wh; ++y) {
for (long x = 0; x < ww; ++x) {
if (role[y * ww + x] != RING) continue;
const uint8_t *t = rgba + (size_t)(wy0 + y) * stride + (size_t)(wx0 + x) * 4;
const uint8_t *s = rgba + (size_t)(wy0 + y + dy) * stride + (size_t)(wx0 + x + dx) * 4;
for (int c = 0; c < 4; ++c) { double d = (double)t[c] - s[c]; sum += d * d; }
++n;
}
}
return n ? sum / n : INFINITY;
}
// Solves for smooth values over HOLE pixels, fixed to the RING values around them. A coarser
// copy is solved first and used as the starting point, so large spots settle in few passes.
static void heal_solve(float *value, const uint8_t *role, long w, long h, int depth) {
int iterations = 300;
if (w > 32 && h > 32 && depth < 16) {
long cw = (w + 1) / 2, ch = (h + 1) / 2;
float *coarse = calloc((size_t)(cw * ch * 4), sizeof(float));
uint8_t *coarseRole = calloc((size_t)(cw * ch), 1);
if (coarse && coarseRole) {
for (long y = 0; y < ch; ++y) {
for (long x = 0; x < cw; ++x) {
int known = 0, hole = 0;
float knownSum[4] = {0, 0, 0, 0}, holeSum[4] = {0, 0, 0, 0};
for (long j = 0; j < 2; ++j) {
for (long i = 0; i < 2; ++i) {
long fx = x * 2 + i, fy = y * 2 + j;
if (fx >= w || fy >= h) continue;
long p = fy * w + fx;
if (role[p] == RING) { ++known; for (int c = 0; c < 4; ++c) knownSum[c] += value[p * 4 + c]; }
else if (role[p] == HOLE) { ++hole; for (int c = 0; c < 4; ++c) holeSum[c] += value[p * 4 + c]; }
}
}
long q = y * cw + x;
if (known) { coarseRole[q] = RING; for (int c = 0; c < 4; ++c) coarse[q * 4 + c] = knownSum[c] / known; }
else if (hole) { coarseRole[q] = HOLE; for (int c = 0; c < 4; ++c) coarse[q * 4 + c] = holeSum[c] / hole; }
}
}
heal_solve(coarse, coarseRole, cw, ch, depth + 1);
for (long y = 0; y < h; ++y) {
for (long x = 0; x < w; ++x) {
long p = y * w + x, q = (y / 2) * cw + x / 2;
if (role[p] == HOLE && coarseRole[q] == HOLE) memcpy(value + p * 4, coarse + q * 4, 4 * sizeof(float));
}
}
iterations = 40;
}
free(coarse);
free(coarseRole);
}
const float omega = 1.8f;
for (int it = 0; it < iterations; ++it) {
for (long y = 0; y < h; ++y) {
for (long x = 0; x < w; ++x) {
long p = y * w + x;
if (role[p] != HOLE) continue;
float sum[4] = {0, 0, 0, 0};
int n = 0;
long neighbors[4][2] = {{x - 1, y}, {x + 1, y}, {x, y - 1}, {x, y + 1}};
for (int k = 0; k < 4; ++k) {
long nx = neighbors[k][0], ny = neighbors[k][1];
if (nx < 0 || ny < 0 || nx >= w || ny >= h) continue;
long q = ny * w + nx;
if (role[q] == OUTSIDE) continue;
for (int c = 0; c < 4; ++c) sum[c] += value[q * 4 + c];
++n;
}
if (!n) continue;
for (int c = 0; c < 4; ++c) value[p * 4 + c] += omega * (sum[c] / n - value[p * 4 + c]);
}
}
}
}
int spot_heal(uint8_t *rgba, const uint8_t *coverage, size_t width, size_t height, size_t stride,
float opacity, int mode, uint32_t seed) {
long W = (long)width, H = (long)height;
long bounds[4];
heal_coverage_bounds(coverage, width, height, width, bounds);
if (bounds[2] <= bounds[0]) return 0;
long bw = bounds[2] - bounds[0], bh = bounds[3] - bounds[1], size = bw > bh ? bw : bh;
long ring = size / 8;
if (ring < 2) ring = 2;
if (ring > 16) ring = 16;
// Work box: the spot plus its ring, clipped to the image.
long wx0 = bounds[0] - ring < 0 ? 0 : bounds[0] - ring, wy0 = bounds[1] - ring < 0 ? 0 : bounds[1] - ring;
long wx1 = bounds[2] + ring > W ? W : bounds[2] + ring, wy1 = bounds[3] + ring > H ? H : bounds[3] + ring;
long ww = wx1 - wx0, wh = wy1 - wy0, wn = ww * wh;
uint8_t *role = calloc((size_t)wn, 1), *near = calloc((size_t)wn, 1);
long *prefix = malloc((size_t)((ww > wh ? ww : wh) + 1) * sizeof(long));
float *value = malloc((size_t)wn * 4 * sizeof(float));
int status = -1;
if (!role || !near || !prefix || !value) goto done;
for (long y = 0; y < wh; ++y)
for (long x = 0; x < ww; ++x)
role[y * ww + x] = coverage[(size_t)(wy0 + y) * width + (size_t)(wx0 + x)] ? HOLE : OUTSIDE;
// The ring: pixels within `ring` of the spot (a square dilation, row pass then column pass).
for (long y = 0; y < wh; ++y) {
prefix[0] = 0;
for (long x = 0; x < ww; ++x) prefix[x + 1] = prefix[x] + (role[y * ww + x] == HOLE);
for (long x = 0; x < ww; ++x) {
long lo = x - ring < 0 ? 0 : x - ring, hi = x + ring + 1 > ww ? ww : x + ring + 1;
near[y * ww + x] = prefix[hi] - prefix[lo] > 0;
}
}
for (long x = 0; x < ww; ++x) {
prefix[0] = 0;
for (long y = 0; y < wh; ++y) prefix[y + 1] = prefix[y] + near[y * ww + x];
for (long y = 0; y < wh; ++y) {
long lo = y - ring < 0 ? 0 : y - ring, hi = y + ring + 1 > wh ? wh : y + ring + 1;
if (role[y * ww + x] == OUTSIDE && prefix[hi] - prefix[lo] > 0) role[y * ww + x] = RING;
}
}
long ringCount = 0;
for (long p = 0; p < wn; ++p) ringCount += role[p] == RING;
if (!ringCount) { status = 0; goto done; }
// Source patch for Content-Aware and Proximity Match.
long ox = 0, oy = 0;
int haveSource = 0;
if (mode != 1) {
static const double factors[5] = {1.05, 1.35, 1.75, 2.25, 2.8};
int count = mode == 2 ? 2 : 5;
double best = INFINITY;
for (int f = 0; f < count; ++f) {
for (int a = 0; a < 24; ++a) {
double angle = a * M_PI / 12.0;
long dx = lround(cos(angle) * factors[f] * ww), dy = lround(sin(angle) * factors[f] * wh);
double score = heal_score(rgba, stride, role, wx0, wy0, ww, wh, dx, dy, W, H);
if (!isfinite(score)) continue;
score *= mode == 2 ? 1.0 + 0.6 * f : 1.0 + 0.1 * f; // nearer patches win ties
if (score < best) { best = score; ox = dx; oy = dy; }
}
}
if (isfinite(best)) {
// Fine-tune the alignment so repeating texture lines up.
long cx = ox, cy = oy;
double refined = heal_score(rgba, stride, role, wx0, wy0, ww, wh, cx, cy, W, H);
for (long j = -3; j <= 3; ++j) {
for (long i = -3; i <= 3; ++i) {
double score = heal_score(rgba, stride, role, wx0, wy0, ww, wh, cx + i, cy + j, W, H);
if (score < refined) { refined = score; ox = cx + i; oy = cy + j; }
}
}
haveSource = 1;
}
}
// Membrane: the edge difference between the original and the patch (or the original itself
// for a smooth fill), spread across the spot.
double mean[4] = {0, 0, 0, 0}, detail[3] = {0, 0, 0};
for (long y = 0; y < wh; ++y) {
for (long x = 0; x < ww; ++x) {
long p = y * ww + x;
if (role[p] != RING) { memset(value + p * 4, 0, 4 * sizeof(float)); continue; }
long ix = wx0 + x, iy = wy0 + y;
const uint8_t *t = rgba + (size_t)iy * stride + (size_t)ix * 4;
const uint8_t *s = haveSource ? rgba + (size_t)(iy + oy) * stride + (size_t)(ix + ox) * 4 : NULL;
for (int c = 0; c < 4; ++c) {
value[p * 4 + c] = (float)t[c] - (s ? s[c] : 0);
mean[c] += value[p * 4 + c];
}
if (!haveSource) {
// Fine detail around the spot: each pixel against the average of its neighbours.
for (int c = 0; c < 3; ++c) {
double around = 0; int n = 0;
long offsets[4][2] = {{ix - 1, iy}, {ix + 1, iy}, {ix, iy - 1}, {ix, iy + 1}};
for (int k = 0; k < 4; ++k) {
if (offsets[k][0] < 0 || offsets[k][1] < 0 || offsets[k][0] >= W || offsets[k][1] >= H) continue;
around += rgba[(size_t)offsets[k][1] * stride + (size_t)offsets[k][0] * 4 + c];
++n;
}
if (n) { double d = t[c] - around / n; detail[c] += d * d; }
}
}
}
}
for (int c = 0; c < 4; ++c) mean[c] /= ringCount;
for (long p = 0; p < wn; ++p)
if (role[p] == HOLE) for (int c = 0; c < 4; ++c) value[p * 4 + c] = (float)mean[c];
heal_solve(value, role, ww, wh, 0);
for (int c = 0; c < 3; ++c) detail[c] = sqrt(detail[c] / ringCount) * 0.9;
for (long y = 0; y < wh; ++y) {
for (long x = 0; x < ww; ++x) {
long p = y * ww + x;
if (role[p] != HOLE) continue;
long ix = wx0 + x, iy = wy0 + y;
uint8_t *t = rgba + (size_t)iy * stride + (size_t)ix * 4;
const uint8_t *s = haveSource ? rgba + (size_t)(iy + oy) * stride + (size_t)(ix + ox) * 4 : NULL;
double amount = coverage[(size_t)iy * width + (size_t)ix] / 255.0 * opacity;
double grain = 0;
if (!haveSource) {
uint32_t key = heal_hash(seed ^ heal_hash((uint32_t)(iy * W + ix)));
double u1 = heal_unit(key), u2 = heal_unit(key ^ 0x68e31da4U);
grain = sqrt(-2.0 * log(1.0 - u1)) * cos(2.0 * M_PI * u2);
}
double out[4];
for (int c = 0; c < 4; ++c) {
double healed = (s ? s[c] : 0) + value[p * 4 + c] + (c < 3 ? grain * detail[c] : 0);
out[c] = t[c] + (healed - t[c]) * amount;
}
double alpha = out[3] < 0 ? 0 : out[3] > 255 ? 255 : out[3];
t[3] = (uint8_t)lround(alpha);
for (int c = 0; c < 3; ++c) {
double v = out[c] < 0 ? 0 : out[c] > t[3] ? t[3] : out[c];
t[c] = (uint8_t)lround(v);
}
}
}
status = 0;
done:
free(role);
free(near);
free(prefix);
free(value);
return status;
}
+19
View File
@@ -0,0 +1,19 @@
#ifndef HealPixels_h
#define HealPixels_h
#include <stdint.h>
#include <stddef.h>
// Half-open bounds of nonzero bytes in a gray bitmap; all zero when empty.
void heal_coverage_bounds(const uint8_t *gray, size_t width, size_t height, size_t stride, long bounds[4]);
// Spot healing, in place, over premultiplied RGBA (4 bytes per pixel, `stride` bytes per row).
// `coverage` (width * height bytes, 0–255) marks what to heal.
// mode 0, Content-Aware: copies texture from the nearby patch whose surrounding ring of pixels
// best matches the ring around the spot;
// mode 1, Create Texture: fills smoothly from the spot's edges and adds grain matching the
// detail around it;
// mode 2, Proximity Match: like 0, but takes the closest good patch.
// Copied texture is blended so it meets the surrounding tone exactly: the difference along the
// spot's edge is spread smoothly across it (a membrane fill). The result replaces the original
// by coverage × opacity. Returns 0, or -1 when memory runs out.
int spot_heal(uint8_t *rgba, const uint8_t *coverage, size_t width, size_t height, size_t stride,
float opacity, int mode, uint32_t seed);
#endif
+37
View File
@@ -0,0 +1,37 @@
#include "LensPixels.h"
#include <math.h>
void lens_distort(const uint8_t *source, uint8_t *destination, size_t width, size_t height, size_t stride, double k) {
double cx = width * 0.5, cy = height * 0.5;
double halfDiagonal2 = cx * cx + cy * cy;
for (size_t y = 0; y < height; ++y) {
double dy = y + 0.5 - cy;
uint8_t *out = destination + y * stride;
for (size_t x = 0; x < width; ++x) {
double dx = x + 0.5 - cx;
double scale = 1.0 - k * (dx * dx + dy * dy) / halfDiagonal2;
// Source position in pixel-center coordinates.
double sx = cx + dx * scale - 0.5, sy = cy + dy * scale - 0.5;
double fx0 = floor(sx), fy0 = floor(sy);
double fx = sx - fx0, fy = sy - fy0;
long x0 = (long)fx0, y0 = (long)fy0;
double sums[4] = {0, 0, 0, 0};
for (int j = 0; j < 2; ++j) {
long row = y0 + j;
if (row < 0 || row >= (long)height) continue;
double wy = j ? fy : 1 - fy;
if (wy == 0) continue;
const uint8_t *line = source + (size_t)row * stride;
for (int i = 0; i < 2; ++i) {
long column = x0 + i;
if (column < 0 || column >= (long)width) continue;
double weight = wy * (i ? fx : 1 - fx);
if (weight == 0) continue;
const uint8_t *p = line + (size_t)column * 4;
for (int c = 0; c < 4; ++c) sums[c] += weight * p[c];
}
}
for (int c = 0; c < 4; ++c) out[x * 4 + c] = (uint8_t)lround(sums[c]);
}
}
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef LensPixels_h
#define LensPixels_h
#include <stdint.h>
#include <stddef.h>
// Radial lens distortion over premultiplied RGBA (4 bytes per pixel, `stride` bytes per row, same
// layout for source and destination). Each destination pixel samples the source bilinearly at
// its offset from the image center scaled by (1 - k * r²), where r is that offset relative to the
// half-diagonal: k > 0 pulls samples inward (straightens barrel distortion, corners crop), k < 0
// pushes them outward (straightens pincushion distortion, corners turn transparent). Pixels
// outside the source are transparent. k = 0 copies the source exactly.
void lens_distort(const uint8_t *source, uint8_t *destination, size_t width, size_t height, size_t stride, double k);
#endif
+28
View File
@@ -0,0 +1,28 @@
#include "LevelsPixels.h"
#include <math.h>
void levels_apply(uint8_t *pixels, size_t count, const float *tables) {
for (size_t i=0; i<count; ++i) {
uint8_t *p = pixels + i*4;
float alpha = p[3];
if (!alpha) continue;
for (int channel=0; channel<3; ++channel) {
float x = fminf(255, p[channel]*255.0f/alpha);
int lo = (int)x, hi = lo < 255 ? lo+1 : 255;
const float *table = tables + channel*256;
float result = table[lo] + (table[hi]-table[lo])*(x-lo);
p[channel] = (uint8_t)fminf(alpha, fmaxf(0, roundf(result*alpha)));
}
}
}
void levels_histogram(const uint8_t *pixels, const uint8_t *coverage, size_t count, double *bins) {
for (size_t i=0; i<count; ++i) {
const uint8_t *p = pixels+i*4;
if (!p[3]) continue;
double weight = p[3]/255.0 * (coverage ? coverage[i]/255.0 : 1);
for (int channel=0; channel<3; ++channel) {
int value = (int)fmin(255, round(p[channel]*255.0/p[3]));
bins[(channel+1)*256+value] += weight;
bins[value] += weight/3.0;
}
}
}
+4
View File
@@ -0,0 +1,4 @@
#include <stdint.h>
#include <stddef.h>
void levels_apply(uint8_t *pixels, size_t count, const float *tables);
void levels_histogram(const uint8_t *pixels, const uint8_t *coverage, size_t count, double *bins);
+41
View File
@@ -0,0 +1,41 @@
#include "NoisePixels.h"
#include <math.h>
// A well-mixed 32-bit hash, so neighbouring pixels get unrelated values.
static inline uint32_t noise_hash(uint32_t x) {
x ^= x >> 16; x *= 0x7feb352dU;
x ^= x >> 15; x *= 0x846ca68bU;
x ^= x >> 16;
return x;
}
// Uniform in [0, 1).
static inline float noise_unit(uint32_t key) { return (float)(noise_hash(key) >> 8) * (1.0f / 16777216.0f); }
void noise_add(uint8_t *rgba, size_t width, size_t height, size_t stride,
float amount, int gaussian, int monochromatic, uint32_t seed) {
float spread = amount / 100.0f * 127.5f;
for (size_t y = 0; y < height; ++y) {
uint8_t *row = rgba + y * stride;
for (size_t x = 0; x < width; ++x) {
uint8_t *p = row + x * 4;
unsigned alpha = p[3];
if (!alpha) continue;
uint32_t base = noise_hash(seed ^ noise_hash((uint32_t)(y * width + x)));
for (int c = 0; c < 3; ++c) {
uint32_t key = monochromatic ? base : base + (uint32_t)c * 0x9e3779b9U;
float n;
if (gaussian) {
// Box–Muller: two uniform values make one normally distributed one.
float u1 = noise_unit(key), u2 = noise_unit(key ^ 0x68e31da4U);
n = sqrtf(-2.0f * logf(1.0f - u1)) * cosf(6.2831853f * u2) * spread * (2.0f / 3.0f);
} else {
n = (noise_unit(key) * 2.0f - 1.0f) * spread;
}
float value = (float)p[c] * 255.0f / (float)alpha + n;
value = value < 0 ? 0 : value > 255 ? 255 : value;
p[c] = (uint8_t)lroundf(value * (float)alpha / 255.0f);
}
}
}
}
+12
View File
@@ -0,0 +1,12 @@
#ifndef NoisePixels_h
#define NoisePixels_h
#include <stdint.h>
#include <stddef.h>
// Adds noise to the color of premultiplied RGBA pixels (4 bytes per pixel, `stride` bytes per
// row), leaving alpha untouched and fully transparent pixels alone. `amount` is Photoshop's
// percentage: uniform noise spans ±amount% of half the range, Gaussian noise has a standard
// deviation of two thirds of that. Monochromatic adds the same value to all three channels.
// Each pixel's noise depends only on its position and `seed`, so the same seed gives the same grain.
void noise_add(uint8_t *rgba, size_t width, size_t height, size_t stride,
float amount, int gaussian, int monochromatic, uint32_t seed);
#endif
+174
View File
@@ -0,0 +1,174 @@
#include "WandPixels.h"
#include <stdlib.h>
#include <string.h>
enum { EAST = 1, SOUTH = 2, WEST = 4, NORTH = 8 };
// Outlines with more pixel edges than this are refused: the path would be too slow to draw.
static const size_t wand_edge_limit = 8000000;
static inline int wand_matches(const uint8_t *p, const int reference[4], int tolerance) {
for (int c = 0; c < 4; ++c) {
int d = (int)p[c] - reference[c];
if (d < -tolerance || d > tolerance) return 0;
}
return 1;
}
long wand_mask(const uint8_t *rgba, size_t width, size_t height, size_t stride,
size_t seedX, size_t seedY, size_t radius, int tolerance, int contiguous, uint8_t *mask) {
if (!width || !height) return 0;
memset(mask, 0, width * height);
if (seedX >= width || seedY >= height) return 0;
size_t x0 = seedX > radius ? seedX - radius : 0, x1 = seedX + radius < width ? seedX + radius : width - 1;
size_t y0 = seedY > radius ? seedY - radius : 0, y1 = seedY + radius < height ? seedY + radius : height - 1;
unsigned long sums[4] = {0, 0, 0, 0}, samples = 0;
for (size_t y = y0; y <= y1; ++y)
for (size_t x = x0; x <= x1; ++x, ++samples)
for (int c = 0; c < 4; ++c) sums[c] += rgba[y * stride + x * 4 + c];
int reference[4];
for (int c = 0; c < 4; ++c) reference[c] = (int)((sums[c] + samples / 2) / samples);
long count = 0;
if (!contiguous) {
for (size_t y = 0; y < height; ++y) {
const uint8_t *row = rgba + y * stride;
uint8_t *out = mask + y * width;
for (size_t x = 0; x < width; ++x)
if (wand_matches(row + x * 4, reference, tolerance)) { out[x] = 255; ++count; }
}
return count;
}
// Scanline flood fill: each popped seed fills its whole horizontal run, then pushes one
// seed per matching run in the rows directly above and below it.
size_t capacity = 4096, top = 1;
size_t *stack = malloc(capacity * 2 * sizeof(size_t));
if (!stack) return -1;
stack[0] = seedX;
stack[1] = seedY;
while (top) {
--top;
size_t x = stack[top * 2], y = stack[top * 2 + 1];
const uint8_t *row = rgba + y * stride;
uint8_t *out = mask + y * width;
if (out[x] || !wand_matches(row + x * 4, reference, tolerance)) continue;
size_t left = x, right = x;
while (left > 0 && !out[left - 1] && wand_matches(row + (left - 1) * 4, reference, tolerance)) --left;
while (right + 1 < width && !out[right + 1] && wand_matches(row + (right + 1) * 4, reference, tolerance)) ++right;
memset(out + left, 255, right - left + 1);
count += (long)(right - left + 1);
for (int side = 0; side < 2; ++side) {
if (side == 0 ? y == 0 : y + 1 >= height) continue;
size_t ny = side == 0 ? y - 1 : y + 1;
const uint8_t *nrow = rgba + ny * stride;
const uint8_t *nout = mask + ny * width;
int inRun = 0;
for (size_t nx = left; nx <= right; ++nx) {
int candidate = !nout[nx] && wand_matches(nrow + nx * 4, reference, tolerance);
if (candidate && !inRun) {
if (top == capacity) {
size_t *grown = realloc(stack, capacity * 4 * sizeof(size_t));
if (!grown) { free(stack); return -1; }
stack = grown;
capacity *= 2;
}
stack[top * 2] = nx;
stack[top * 2 + 1] = ny;
++top;
}
inRun = candidate;
}
}
}
free(stack);
return count;
}
// Headings, clockwise on screen (y grows downward): east, south, west, north.
static inline int turn_right(int d) { return d == NORTH ? EAST : d << 1; }
static inline int turn_left(int d) { return d == EAST ? NORTH : d >> 1; }
int wand_trace(const uint8_t *mask, size_t width, size_t height,
int32_t **points, size_t *pointCount, int32_t **loops, size_t *loopCount) {
*points = NULL;
*loops = NULL;
*pointCount = 0;
*loopCount = 0;
if (!width || !height) return 0;
if (width >= INT32_MAX || height >= INT32_MAX) return -1;
// Each vertex of the (width + 1) × (height + 1) grid records the directed boundary edges
// leaving it: a selected pixel's unselected sides, walked clockwise around the pixel.
size_t stride = width + 1, vertices = stride * (height + 1), edges = 0;
uint8_t *out = calloc(vertices, 1);
if (!out) return -1;
for (size_t y = 0; y < height; ++y) {
const uint8_t *row = mask + y * width;
for (size_t x = 0; x < width; ++x) {
if (!row[x]) continue;
if (y == 0 || !mask[(y - 1) * width + x]) { out[y * stride + x] |= EAST; ++edges; }
if (x + 1 == width || !row[x + 1]) { out[y * stride + x + 1] |= SOUTH; ++edges; }
if (y + 1 == height || !mask[(y + 1) * width + x]) { out[(y + 1) * stride + x + 1] |= WEST; ++edges; }
if (x == 0 || !row[x - 1]) { out[(y + 1) * stride + x] |= NORTH; ++edges; }
}
if (edges > wand_edge_limit) { free(out); return -2; }
}
size_t pointCapacity = 1024, loopCapacity = 256, np = 0, nl = 0;
int32_t *pts = malloc(pointCapacity * 2 * sizeof(int32_t));
int32_t *lens = malloc(loopCapacity * sizeof(int32_t));
if (!pts || !lens) goto fail;
for (size_t start = 0; start < vertices; ++start) {
while (out[start]) {
size_t first = np, v = start;
int heading = 0, initial = 0;
do {
int bits = out[v], d;
// Where two loops meet at a corner, turning right keeps them apart.
if (!heading) d = bits & -bits;
else if (bits & turn_right(heading)) d = turn_right(heading);
else if (bits & heading) d = heading;
else if (bits & turn_left(heading)) d = turn_left(heading);
else d = bits & -bits;
if (!d) break;
out[v] &= (uint8_t)~d;
if (d != heading) {
if (np == pointCapacity) {
int32_t *grown = realloc(pts, pointCapacity * 4 * sizeof(int32_t));
if (!grown) goto fail;
pts = grown;
pointCapacity *= 2;
}
pts[np * 2] = (int32_t)(v % stride);
pts[np * 2 + 1] = (int32_t)(v / stride);
++np;
}
if (!heading) initial = d;
heading = d;
v = d == EAST ? v + 1 : d == WEST ? v - 1 : d == SOUTH ? v + stride : v - stride;
} while (v != start);
// The start is a corner unless the loop arrives on the heading it left with.
if (heading == initial && np > first) {
memmove(pts + first * 2, pts + (first + 1) * 2, (np - first - 1) * 2 * sizeof(int32_t));
--np;
}
if (nl == loopCapacity) {
int32_t *grown = realloc(lens, loopCapacity * 2 * sizeof(int32_t));
if (!grown) goto fail;
lens = grown;
loopCapacity *= 2;
}
lens[nl++] = (int32_t)(np - first);
}
}
free(out);
*points = pts;
*loops = lens;
*pointCount = np;
*loopCount = nl;
return 0;
fail:
free(out);
free(pts);
free(lens);
return -1;
}
+21
View File
@@ -0,0 +1,21 @@
#ifndef WandPixels_h
#define WandPixels_h
#include <stdint.h>
#include <stddef.h>
// Magic Wand match over premultiplied RGBA (4 bytes per pixel, `stride` bytes per row, rows
// top-down). The reference color is the average over a (2 * radius + 1)² square around the
// seed, clipped to the image. A pixel matches when every channel, alpha included, is within
// `tolerance` of it. Contiguous fills 4-connected from the seed (nothing when the seed itself
// doesn't match); otherwise every matching pixel. Writes 255 for selected, 0 elsewhere, into
// `mask` (width * height bytes). Returns the number selected, or -1 when memory runs out.
long wand_mask(const uint8_t *rgba, size_t width, size_t height, size_t stride,
size_t seedX, size_t seedY, size_t radius, int tolerance, int contiguous, uint8_t *mask);
// Outline of the nonzero pixels of `mask`, along pixel edges, as closed loops of corner points
// (x, y pairs in pixel-edge coordinates). Outer boundaries run clockwise and holes
// counterclockwise in top-left coordinates, so the winding rule fills exactly those pixels.
// `points` receives 2 * pointCount values and `loops` each loop's corner count; both are
// malloc'd and must be freed. Returns 0 on success, -1 when memory runs out, and -2 when the
// outline is too detailed to be worth drawing.
int wand_trace(const uint8_t *mask, size_t width, size_t height,
int32_t **points, size_t *pointCount, int32_t **loops, size_t *loopCount);
#endif