581 lines
20 KiB
C
581 lines
20 KiB
C
/*
|
|
resynth - A program for resynthesizing textures.
|
|
modified by Connor Olding, 2016
|
|
Copyright (C) 2000 2008 Paul Francis Harrison
|
|
Copyright (C) 2002 Laurent Despeyroux
|
|
Copyright (C) 2002 David Rodríguez García
|
|
|
|
This program is licensed under the terms of the GNU General Public
|
|
License (version 2), and is distributed without any warranty.
|
|
You should have received a copy of the license with the program.
|
|
If not, visit <http://gnu.org/licenses/> to obtain one.
|
|
*/
|
|
|
|
#include <limits.h>
|
|
#include <math.h> // for log (neglog_cauchy) used in pixel diff. calculations
|
|
#include <stdbool.h> // we're targetting C11 anyway, may as well use it
|
|
#include <stdint.h> // for uint8_t for pixel data
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h> // for file extension mangling
|
|
#include <time.h> // for time(0) as a random seed
|
|
|
|
// decide which features we want from stb_image.
|
|
// this should cover the most common formats.
|
|
#define STB_IMAGE_IMPLEMENTATION
|
|
#define STB_IMAGE_STATIC
|
|
#define STBI_ONLY_JPEG
|
|
#define STBI_ONLY_PNG
|
|
#define STBI_ONLY_BMP
|
|
#define STBI_ONLY_GIF
|
|
#include "stb_image.h"
|
|
|
|
// likewise for stb_image_write. by using the static keyword,
|
|
// any unused formats and functions can be stripped from the resulting binary.
|
|
// we only use png (stbi_write_png) in this case.
|
|
#define STB_IMAGE_WRITE_IMPLEMENTATION
|
|
#define STB_IMAGE_WRITE_STATIC
|
|
#pragma GCC diagnostic push
|
|
#pragma GCC diagnostic ignored "-Wmissing-prototypes"
|
|
#include "stb_image_write.h"
|
|
#pragma GCC diagnostic pop
|
|
|
|
// this isn't the prettiest way of handling memory errors,
|
|
// but it should suffice for our one-thing one-shot program.
|
|
#define STRETCHY_BUFFER_OUT_OF_MEMORY \
|
|
fprintf(stderr, "fatal error: ran out of memory in stb__sbgrowf\n"); \
|
|
exit(1);
|
|
// provides vector<>-like arrays of variable size.
|
|
#include "stretchy_buffer.h"
|
|
|
|
// for command-line argument parsing
|
|
#include "kyaa.h"
|
|
#include "kyaa_extra.h"
|
|
|
|
// rand() is neither consistent across platforms
|
|
// nor guaranteed to have desirable properties,
|
|
// so we use this random number generator instead.
|
|
#define RND_U32 uint32_t
|
|
#define RND_U64 uint64_t
|
|
#define RND_IMPLEMENTATION
|
|
#include "rnd.h"
|
|
rnd_pcg_t pcg;
|
|
|
|
// convenience macros. hopefully these names don't interfere
|
|
// with any defined in the standard library headers on any system.
|
|
// it's technically not an error to redefine macros anyway.
|
|
#define MIN(a, b) ((a) < (b) ? (a) : (b))
|
|
#define MAX(a, b) ((a) > (b) ? (a) : (b))
|
|
#define CLAMP(x, l, u) (MIN(MAX(x, l), u))
|
|
#define CLAMPV(x, l, u) x = CLAMP(x, l, u)
|
|
#define LEN(a) (sizeof(a) / sizeof((a)[0]))
|
|
|
|
// this macro lets us state how much memory we want a pointer to employ,
|
|
// while freeing its old memory as needed.
|
|
// note that this expects the given variable to be initialized to NULL
|
|
// for its first allocation; using an uninitialized pointer is erroneous.
|
|
// (this should probably be renamed to something more meaningful)
|
|
#define MEMORY(a, size) \
|
|
do { \
|
|
if (a) (a) = (free(a), NULL); \
|
|
if (size > 0) (a) = (typeof(a))(calloc(size, sizeof((a)[0]))); \
|
|
} while (0) \
|
|
|
|
// a simple extension to stretchy_buffer.h:
|
|
// a macro that frees and nulls a pointer at the same time,
|
|
// which should be safer and easier to program with.
|
|
#define sb_freeset(a) ((a) = (sb_free(a), NULL))
|
|
|
|
// in some cases, we must declare functions as static to prevent
|
|
// their symbols being exported, thus truly allowing them to be inlined.
|
|
#define INLINE static inline
|
|
|
|
// end of generic boilerplate, here's the actual program:
|
|
|
|
typedef struct coord {
|
|
int x, y;
|
|
} Coord;
|
|
|
|
INLINE Coord coord_add(const Coord a, const Coord b) {
|
|
return (Coord){a.x + b.x, a.y + b.y};
|
|
}
|
|
|
|
INLINE Coord coord_sub(const Coord a, const Coord b) {
|
|
return (Coord){a.x - b.x, a.y - b.y};
|
|
}
|
|
|
|
static int coord_compare(const void *v_a, const void *v_b) {
|
|
const Coord *a = (Coord *) v_a;
|
|
const Coord *b = (Coord *) v_b;
|
|
return (a->x * a->x + a->y * a->y) - (b->x * b->x + b->y * b->y);
|
|
}
|
|
|
|
typedef uint8_t Pixel;
|
|
|
|
typedef union {
|
|
Pixel v[4];
|
|
struct {
|
|
Pixel r, g, b, a;
|
|
};
|
|
} Pixel32;
|
|
|
|
typedef struct {
|
|
bool has_value, has_source;
|
|
Coord source;
|
|
} Status;
|
|
|
|
typedef struct {
|
|
int width, height, depth;
|
|
} Image;
|
|
|
|
// convenience macros to simplify image handling.
|
|
// note that these secretly expect an image##_array variable to exist.
|
|
#define IMAGE_RESIZE(image, w, h, d) \
|
|
do { \
|
|
image.width = w; \
|
|
image.height = h; \
|
|
image.depth = d; \
|
|
MEMORY(image##_array, w * h * d); \
|
|
} while (0) \
|
|
|
|
#define image_at(image, x, y) (image##_array + (y * image.width + x) * image.depth)
|
|
#define image_atc(image, coord) image_at(image, coord.x, coord.y)
|
|
|
|
typedef struct {
|
|
bool h_tile, v_tile;
|
|
double autism;
|
|
int neighbors, tries;
|
|
int magic;
|
|
} Parameters;
|
|
|
|
INLINE bool wrap_or_clip(const Parameters parameters, const Image image,
|
|
Coord *point) {
|
|
// like modulo, with optional bounds checking.
|
|
// (perhaps we should just use modulo if h_tile and v_tile are both true?)
|
|
while (point->x < 0) {
|
|
if (parameters.h_tile) point->x += image.width;
|
|
else return false;
|
|
}
|
|
while (point->x >= image.width) {
|
|
if (parameters.h_tile) point->x -= image.width;
|
|
else return false;
|
|
}
|
|
while (point->y < 0) {
|
|
if (parameters.v_tile) point->y += image.height;
|
|
else return false;
|
|
}
|
|
while (point->y >= image.height) {
|
|
if (parameters.v_tile) point->y -= image.height;
|
|
else return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
typedef struct {
|
|
int input_bytes;
|
|
// note that these variables must exist alongside their "_array"s
|
|
// for the image macros to work.
|
|
Image data, corpus, status;
|
|
Pixel *data_array, *corpus_array;
|
|
Status *status_array;
|
|
Coord *data_points, *corpus_points, *sorted_offsets;
|
|
Image tried;
|
|
int *tried_array;
|
|
|
|
Coord *neighbors;
|
|
Pixel32 *neighbor_values;
|
|
Status **neighbor_statuses;
|
|
int n_neighbors;
|
|
|
|
int *diff_table; // (might be more efficient to store as uint16_t?)
|
|
|
|
int best;
|
|
Coord best_point;
|
|
} Resynth_state;
|
|
|
|
static void state_free(Resynth_state *s) {
|
|
sb_freeset(s->data_points);
|
|
sb_freeset(s->corpus_points);
|
|
sb_freeset(s->sorted_offsets);
|
|
MEMORY(s->diff_table, 0);
|
|
MEMORY(s->neighbors, 0);
|
|
MEMORY(s->neighbor_values, 0);
|
|
MEMORY(s->neighbor_statuses, 0);
|
|
MEMORY(s->data_array, 0);
|
|
MEMORY(s->corpus_array, 0);
|
|
MEMORY(s->status_array, 0);
|
|
MEMORY(s->tried_array, 0);
|
|
}
|
|
|
|
static double neglog_cauchy(double x) {
|
|
return log(x * x + 1.0);
|
|
}
|
|
|
|
static void make_offset_list(Resynth_state *s) {
|
|
// generate a vector of x,y offsets used to search around any given pixel.
|
|
// this is constrained by the minimum image size to prevent overlapping.
|
|
int width = MIN(s->corpus.width, s->data.width);
|
|
int height = MIN(s->corpus.height, s->data.height);
|
|
|
|
sb_freeset(s->sorted_offsets);
|
|
for (int y = -height + 1; y < height; y++) {
|
|
for (int x = -width + 1; x < width; x++) {
|
|
Coord c = {x, y};
|
|
sb_push(s->sorted_offsets, c);
|
|
}
|
|
}
|
|
|
|
// sort offsets in ascending distance from the center zero-point.
|
|
// the order of equal distances is undefined. (it doesn't seem to matter)
|
|
// (for a more detailed description of what's going on, refer to the README)
|
|
qsort(s->sorted_offsets, sb_count(s->sorted_offsets),
|
|
sizeof(Coord), coord_compare);
|
|
}
|
|
|
|
INLINE void try_point(Resynth_state *s, const Coord point) {
|
|
// consider a candidate pixel for the best-fit by considering its neighbors.
|
|
int sum = 0;
|
|
|
|
for (int i = 0; i < s->n_neighbors; i++) {
|
|
Coord off_point = coord_add(point, s->neighbors[i]);
|
|
|
|
int diff = 0;
|
|
if (off_point.x < 0 || off_point.y < 0 ||
|
|
off_point.x >= s->corpus.width || off_point.y >= s->corpus.height) {
|
|
// penalize edges, assuming the corpus image doesn't wrap cleanly.
|
|
diff = s->diff_table[0] * s->input_bytes;
|
|
} else if (i) {
|
|
const Pixel *corpus_pixel = image_atc(s->corpus, off_point);
|
|
const Pixel *data_pixel = s->neighbor_values[i].v;
|
|
for (int j = 0; j < s->input_bytes; j++) {
|
|
diff += s->diff_table[256 + data_pixel[j] - corpus_pixel[j]];
|
|
}
|
|
}
|
|
|
|
#ifdef NDEBUG
|
|
sum += diff;
|
|
#else
|
|
if (__builtin_add_overflow(sum, diff, &sum)) {
|
|
fprintf(stderr, "integer overflow at (%i,%i) + (%i,%i)\n",
|
|
point.x, point.y, s->neighbors[i].x, s->neighbors[i].y);
|
|
fprintf(stderr, "diff: %i\n", diff);
|
|
exit(1);
|
|
}
|
|
#endif
|
|
if (sum >= s->best) return;
|
|
}
|
|
|
|
s->best = sum;
|
|
s->best_point = point;
|
|
}
|
|
|
|
INLINE void resynth__init(Resynth_state *s, Parameters parameters) {
|
|
sb_freeset(s->data_points);
|
|
sb_freeset(s->corpus_points);
|
|
sb_freeset(s->sorted_offsets);
|
|
|
|
// (iirc i opted to put diff_table on heap to keep Resynth_state small)
|
|
MEMORY(s->diff_table, 512);
|
|
MEMORY(s->neighbors, parameters.neighbors);
|
|
MEMORY(s->neighbor_values, parameters.neighbors);
|
|
MEMORY(s->neighbor_statuses, parameters.neighbors);
|
|
|
|
IMAGE_RESIZE(s->status, s->data.width, s->data.height, 1);
|
|
|
|
// set default values and allocate points to shuffle later.
|
|
for (int y = 0; y < s->status.height; y++) {
|
|
for (int x = 0; x < s->status.width; x++) {
|
|
// (this might be redundant since this memory is calloc'd)
|
|
image_at(s->status, x, y)->has_source = false;
|
|
image_at(s->status, x, y)->has_value = false;
|
|
|
|
Coord coord = {x, y};
|
|
sb_push(s->data_points, coord);
|
|
}
|
|
}
|
|
|
|
// likewise for the corpus.
|
|
for (int y = 0; y < s->corpus.height; y++) {
|
|
for (int x = 0; x < s->corpus.width; x++) {
|
|
Coord coord = {x, y};
|
|
sb_push(s->corpus_points, coord);
|
|
}
|
|
}
|
|
|
|
if (!sb_count(s->corpus_points) || !sb_count(s->data_points)) {
|
|
fprintf(stderr, "invalid sizes\n");
|
|
fprintf(stderr, "corpus: %i\n", sb_count(s->corpus_points));
|
|
fprintf(stderr, "data: %i\n", sb_count(s->data_points));
|
|
return;
|
|
}
|
|
|
|
make_offset_list(s);
|
|
|
|
// precompute how "different" a pixel value is from another.
|
|
// this greatly affects how apparent any seams are in the synthesized image.
|
|
// this is done per 8-bit channel, so only 256 * 2 values are needed.
|
|
// since we can't use negative indices, we pretend index 256 is 0 instead.
|
|
// (you could try adding CIELAB heuristics, but this seems robust enough)
|
|
if (parameters.autism > 0) for (int i = -256; i < 256; i++) {
|
|
double value = neglog_cauchy(i / 256.0 / parameters.autism) /
|
|
neglog_cauchy(1.0 / parameters.autism) * 65536.0;
|
|
s->diff_table[256 + i] = (int)(value);
|
|
} else for (int i = -256; i < 256; i++) {
|
|
s->diff_table[256 + i] = (int)(i != 0) * 65536;
|
|
}
|
|
|
|
const int data_area = sb_count(s->data_points);
|
|
|
|
// shuffle the data points in-place.
|
|
for (int i = 0; i < data_area; i++) {
|
|
int j = rnd_pcg_range(&pcg, 0, data_area - 1);
|
|
Coord temp = s->data_points[i];
|
|
s->data_points[i] = s->data_points[j];
|
|
s->data_points[j] = temp;
|
|
}
|
|
|
|
// polishing improves pixels chosen early in the algorithm
|
|
// by reconsidering them after the output image has been filled.
|
|
// this greatly reduces the "sparklies" in the resulting image.
|
|
// this is achieved by appending the first n data points to the end.
|
|
// n is reduced exponentially by "magic" until it's less than 1.
|
|
if (parameters.magic) for (int n = data_area; n > 0;) {
|
|
n = n * parameters.magic / 256;
|
|
for (int i = 0; i < n; i++) {
|
|
sb_push(s->data_points, s->data_points[i]);
|
|
}
|
|
}
|
|
|
|
// prepare an array of neighbors we've already computed the difference of.
|
|
// this is a simple optimization and isn't critical to the algorithm.
|
|
// (tried_array is referred to implicitly by macros)
|
|
IMAGE_RESIZE(s->tried, s->corpus.width, s->corpus.height, 1);
|
|
const int corpus_area = s->corpus.width * s->corpus.height;
|
|
for (int i = 0; i < corpus_area; i++) s->tried_array[i] = -1;
|
|
}
|
|
|
|
static void resynth(Resynth_state *s, Parameters parameters) {
|
|
// "resynthesize" an output image from a given input image.
|
|
resynth__init(s, parameters);
|
|
|
|
for (int i = sb_count(s->data_points) - 1; i >= 0; i--) {
|
|
Coord position = s->data_points[i];
|
|
|
|
// this point is guaranteed to have a value after this iteration.
|
|
image_atc(s->status, position)->has_value = true;
|
|
|
|
// collect neighboring pixels as candidates for best-fit.
|
|
// the order we check and collect is relevant, thus "sorted_offsets".
|
|
s->n_neighbors = 0;
|
|
const int sorted_offsets_size = sb_count(s->sorted_offsets);
|
|
for (int j = 0; j < sorted_offsets_size; j++) {
|
|
Coord point = coord_add(position, s->sorted_offsets[j]);
|
|
|
|
if (wrap_or_clip(parameters, s->data, &point) &&
|
|
image_atc(s->status, point)->has_value) {
|
|
s->neighbors[s->n_neighbors] = s->sorted_offsets[j];
|
|
s->neighbor_statuses[s->n_neighbors] =
|
|
image_atc(s->status, point);
|
|
for (int k = 0; k < s->input_bytes; k++) {
|
|
s->neighbor_values[s->n_neighbors].v[k] =
|
|
image_atc(s->data, point)[k];
|
|
}
|
|
s->n_neighbors++;
|
|
if (s->n_neighbors >= parameters.neighbors) break;
|
|
}
|
|
}
|
|
|
|
s->best = INT_MAX;
|
|
|
|
// consider each neighboring pixel collected as a best-fit.
|
|
for (int j = 0; j < s->n_neighbors && s->best != 0; j++) {
|
|
if (s->neighbor_statuses[j]->has_source) {
|
|
Coord point = coord_sub(s->neighbor_statuses[j]->source,
|
|
s->neighbors[j]);
|
|
if (point.x < 0 || point.y < 0 ||
|
|
point.x >= s->corpus.width || point.y >= s->corpus.height) {
|
|
continue;
|
|
}
|
|
// skip computing differences of points
|
|
// we've already done this iteration. not mandatory.
|
|
if (*image_atc(s->tried, point) == i) continue;
|
|
try_point(s, point);
|
|
*image_atc(s->tried, point) = i;
|
|
}
|
|
}
|
|
|
|
// try some random points in the corpus. this is required for
|
|
// choosing the first couple pixels, since they have no neighbors.
|
|
// after that, this step is optional. it can improve subjective quality.
|
|
for (int j = 0; j < parameters.tries && s->best != 0; j++) {
|
|
int random = rnd_pcg_range(&pcg, 0, sb_count(s->corpus_points) - 1);
|
|
try_point(s, s->corpus_points[random]);
|
|
}
|
|
|
|
// finally, copy the best pixel to the output image.
|
|
for (int j = 0; j < s->input_bytes; j++) {
|
|
image_atc(s->data, position)[j] =
|
|
image_atc(s->corpus, s->best_point)[j];
|
|
}
|
|
image_atc(s->status, position)->has_source = true;
|
|
image_atc(s->status, position)->source = s->best_point;
|
|
}
|
|
}
|
|
|
|
static char *manipulate_filename(const char *fn,
|
|
const char *new_extension) {
|
|
#define MAX_LENGTH 256
|
|
int length = strlen(fn);
|
|
if (length > MAX_LENGTH) length = MAX_LENGTH;
|
|
// out_fn must be freed by the caller.
|
|
char *out_fn = (char *)calloc(2 * MAX_LENGTH, 1);
|
|
strncpy(out_fn, fn, length);
|
|
|
|
char *hint = strrchr(out_fn, '.');
|
|
if (hint == NULL) strcpy(out_fn + length, new_extension);
|
|
else strcpy(hint, new_extension);
|
|
|
|
return out_fn;
|
|
#undef MAX_LENGTH
|
|
}
|
|
|
|
static const int disc00[] = {
|
|
// http://oeis.org/A057961
|
|
1, 5, 9, 13, 21, 25, 29, 37,
|
|
45, 49, 57, 61, 69, 81, 89, 97,
|
|
101, 109, 113, 121, 129, 137, 145, 149,
|
|
161, 169, 177, 185, 193, 197, 213, 221,
|
|
225, 233, 241, 249, 253, 261, 277, 285,
|
|
293, 301, 305, 317, 325, 333, 341, 349,
|
|
357, 365, 373, 377, 385, 401, 405, 421,
|
|
429, 437, 441, 457, 465, 473, 481, 489,
|
|
497, 505, 509, 517, 529, 545, 553, 561,
|
|
569, 577, 593, 601, 609, 613, 621, 633,
|
|
641, 657, 665, 673, 681, 697, 709, 717,
|
|
725, 733, 741, 749, 757, 761, 769, 777,
|
|
793, 797, 805, 821, 829, 845, 853, 861,
|
|
869, 877, 885, 889, 901, 917, 925, 933,
|
|
941, 949, 965, 973, 981, 989, 997, 1005,
|
|
1009, 1033, 1041, 1049, 1057, 1069, 1085, 1093
|
|
};
|
|
|
|
int main(int argc, char *argv[]) {
|
|
Resynth_state state = {0};
|
|
Resynth_state *s = &state; // (just for consistency across functions)
|
|
|
|
Parameters parameters = {0};
|
|
parameters.v_tile = true;
|
|
parameters.h_tile = true;
|
|
// blah = our default; // original resynthesizer default
|
|
parameters.magic = 192; // 192 (3/4)
|
|
parameters.autism = 32. / 256.; // 30. / 256.
|
|
parameters.neighbors = 29; // 30
|
|
parameters.tries = 192; // 200 (or 80 in the paper)
|
|
|
|
int scale = 1;
|
|
unsigned long seed = 0;
|
|
|
|
// our main() return value. subtracted by one for each failed image.
|
|
int ret = 0;
|
|
|
|
KYAA_LOOP {
|
|
KYAA_BEGIN
|
|
|
|
KYAA_FLAG_LONG('a', "autism",
|
|
" sensitivity to outliers\n"
|
|
" range: [0,256]; default: 32")
|
|
parameters.autism = (double)(kyaa_long_value) / 256.;
|
|
|
|
KYAA_FLAG_LONG('N', "neighbors",
|
|
" points to use when sampling\n"
|
|
" range: [0,1024]; default: 29")
|
|
parameters.neighbors = kyaa_long_value;
|
|
|
|
KYAA_FLAG_LONG('R', "circle-radius",
|
|
" circle neighborhood radius\n"
|
|
" range: [1,128]; default: [n/a]")
|
|
int radius = kyaa_long_value;
|
|
radius = CLAMP(radius, 1, (int)(LEN(disc00)));
|
|
parameters.neighbors = disc00[radius - 1];
|
|
|
|
KYAA_FLAG_LONG('M', "tries",
|
|
" random points added to candidates\n"
|
|
" range: [0,65536]; default: 192")
|
|
parameters.tries = kyaa_long_value;
|
|
|
|
KYAA_FLAG_LONG('m', "magic",
|
|
" magic constant, affects iterations\n"
|
|
" range: [0,255]; default: 192")
|
|
parameters.magic = kyaa_long_value;
|
|
|
|
KYAA_FLAG_LONG('s', "scale",
|
|
" output size multiplier; negative values set width and height\n"
|
|
" range: [-8192,32]; default: 1")
|
|
scale = kyaa_long_value;
|
|
|
|
KYAA_FLAG_LONG('S', "seed",
|
|
" initial RNG value\n"
|
|
" default: 0 [time(0)]")
|
|
seed = (unsigned long) kyaa_long_value;
|
|
|
|
KYAA_HELP(" {files...}\n"
|
|
" image files to open, resynthesize, and save as {filename}.resynth.png\n"
|
|
" required default: [none]")
|
|
|
|
KYAA_END
|
|
|
|
if (kyaa_read_stdin) {
|
|
fprintf(stderr, "fatal error: reading from stdin is unsupported\n");
|
|
exit(1);
|
|
}
|
|
|
|
CLAMPV(parameters.magic, 0, 255);
|
|
CLAMPV(parameters.autism, 0., 1.);
|
|
CLAMPV(parameters.neighbors, 0, disc00[LEN(disc00) - 1]);
|
|
CLAMPV(parameters.tries, 0, 65536);
|
|
CLAMPV(scale, -8192, 32);
|
|
|
|
char *fn = kyaa_arg;
|
|
|
|
int w, h, d;
|
|
uint8_t *image = stbi_load(fn, &w, &h, &d, 0);
|
|
if (image == NULL) {
|
|
fprintf(stderr, "invalid image: %s\n", fn);
|
|
ret--;
|
|
continue;
|
|
}
|
|
|
|
IMAGE_RESIZE(s->corpus, w, h, d);
|
|
memcpy(s->corpus_array, image, w * h * d);
|
|
|
|
s->input_bytes = MIN(d, 3);
|
|
|
|
{
|
|
int data_w = 256, data_h = 256;
|
|
if (scale > 0) data_w = scale * w, data_h = scale * h;
|
|
if (scale < 0) data_w = -scale, data_h = -scale;
|
|
IMAGE_RESIZE(s->data, data_w, data_h, s->input_bytes);
|
|
}
|
|
|
|
stbi_image_free(image);
|
|
|
|
if (seed) rnd_pcg_seed(&pcg, seed);
|
|
else rnd_pcg_seed(&pcg, time(0));
|
|
resynth(s, parameters);
|
|
|
|
char *out_fn = manipulate_filename(fn, ".resynth.png");
|
|
puts(out_fn);
|
|
int result = stbi_write_png(out_fn, s->data.width, s->data.height,
|
|
s->data.depth, s->data_array, 0);
|
|
if (!result) {
|
|
fprintf(stderr, "failed to write: %s\n", out_fn);
|
|
ret--;
|
|
}
|
|
|
|
free(out_fn);
|
|
}
|
|
|
|
state_free(s);
|
|
|
|
return ret;
|
|
}
|