/*- genpng * * COPYRIGHT: Written by John Cunningham Bowler, 2015. * To the extent possible under law, the author has waived all copyright and * related or neighboring rights to this work. This work is published from: * United States. * * Generate a PNG with an alpha channel, correctly. * * This is a test case generator; the resultant PNG files are only of interest * to those of us who care about whether the edges of circles are green, red, * or yellow. * * The program generates an RGB+Alpha PNG of a given size containing the given * shapes on a transparent background: * * genpng width height { shape } * shape ::= color width shape x1 y1 x2 y2 * * 'color' is: * * black white red green yellow blue brown purple pink orange gray cyan * * The point is to have colors that are linguistically meaningful plus that old * bugbear of the department store dress murders, Cyan, the only color we argue * about. * * 'shape' is: * * circle: an ellipse * square: a rectangle * line: a straight line * * Each shape is followed by four numbers, these are two points in the output * coordinate space (as real numbers) which describe the circle, square, or * line. The shape is filled if it is preceded by 'filled' (not valid for * 'line') or is drawn with a line, in which case the width of the line must * precede the shape. * * The whole set of information can be repeated as many times as desired: * * shape ::= color width shape x1 y1 x2 y2 * * color ::= black|white|red|green|yellow|blue * color ::= brown|purple|pink|orange|gray|cyan * width ::= filled * width ::= <number> * shape ::= circle|square|line * x1 ::= <number> * x2 ::= <number> * y1 ::= <number> * y2 ::= <number> * * The output PNG is generated by down-sampling a 4x supersampled image using * a bi-cubic filter. The bi-cubic has a 2 (output) pixel width, so an 8x8 * array of super-sampled points contribute to each output pixel. The value of * a super-sampled point is found using an unfiltered, aliased, infinite * precision image: Each shape from the last to the first is checked to see if * the point is in the drawn area and, if it is, the color of the point is the * color of the shape and the alpha is 1, if not the previous shape is checked. * * This is an aliased algorithm because no filtering is done; a point is either * inside or outside each shape and 'close' points do not contribute to the * sample. The down-sampling is relied on to correct the error of not using * a filter. * * The line end-caps are 'flat'; they go through the points. The square line * joins are mitres; the outside of the lines are continued to the point of * intersection. */ #include <stddef.h> #include <stdlib.h> #include <string.h> #include <stdio.h> #include <math.h> /* Normally use <png.h> here to get the installed libpng, but this is done to * ensure the code picks up the local libpng implementation: */ #include "../../png.h" #if defined(PNG_SIMPLIFIED_WRITE_SUPPORTED) && defined(PNG_STDIO_SUPPORTED) static const struct color { const char *name; double red; double green; double blue; } colors[] = /* color ::= black|white|red|green|yellow|blue * color ::= brown|purple|pink|orange|gray|cyan */ { { "black", 0, 0, 0 }, { "white", 1, 1, 1 }, { "red", 1, 0, 0 }, { "green", 0, 1, 0 }, { "yellow", 1, 1, 0 }, { "blue", 0, 0, 1 }, { "brown", .5, .125, 0 }, { "purple", 1, 0, 1 }, { "pink", 1, .5, .5 }, { "orange", 1, .5, 0 }, { "gray", 0, .5, .5 }, { "cyan", 0, 1, 1 } }; #define color_count ((sizeof colors)/(sizeof colors[0])) static const struct color * color_of(const char *arg) { int icolor = color_count; while (--icolor >= 0) { if (strcmp(colors[icolor].name, arg) == 0) return colors+icolor; } fprintf(stderr, "genpng: invalid color %s\n", arg); exit(1); } static double width_of(const char *arg) { if (strcmp(arg, "filled") == 0) return 0; else { char *ep = NULL; double w = strtod(arg, &ep); if (ep != NULL && *ep == 0 && w > 0) return w; } fprintf(stderr, "genpng: invalid line width %s\n", arg); exit(1); } static double coordinate_of(const char *arg) { char *ep = NULL; double w = strtod(arg, &ep); if (ep != NULL && *ep == 0) return w; fprintf(stderr, "genpng: invalid coordinate value %s\n", arg); exit(1); } struct arg; /* forward declaration */ typedef int (*shape_fn_ptr)(const struct arg *arg, double x, double y); /* A function to determine if (x,y) is inside the shape. * * There are two implementations: * * inside_fn: returns true if the point is inside * check_fn: returns; * -1: the point is outside the shape by more than the filter width (2) * 0: the point may be inside the shape * +1: the point is inside the shape by more than the filter width */ #define OUTSIDE (-1) #define INSIDE (1) struct arg { const struct color *color; shape_fn_ptr inside_fn; shape_fn_ptr check_fn; double width; /* line width, 0 for 'filled' */ double x1, y1, x2, y2; }; /* IMPLEMENTATION NOTE: * * We want the contribution of each shape to the sample corresponding to each * pixel. This could be obtained by super sampling the image to infinite * dimensions, finding each point within the shape and assigning that a value * '1' while leaving every point outside the shape with value '0' then * downsampling to the image size with sinc; computationally very expensive. * * Approximations are as follows: * * 1) If the pixel coordinate is within the shape assume the sample has the * shape color and is opaque, else assume there is no contribution from * the shape. * * This is the equivalent of aliased rendering or resampling an image with * a block filter. The maximum error in the calculated alpha (which will * always be 0 or 1) is 0.5. * * 2) If the shape is within a square of size 1x1 centered on the pixel assume * that the shape obscures an amount of the pixel equal to its area within * that square. * * This is the equivalent of 'pixel coverage' alpha calculation or resampling * an image with a bi-linear filter. The maximum error is over 0.2, but the * results are often acceptable. * * This can be approximated by applying (1) to a super-sampled image then * downsampling with a bi-linear filter. The error in the super-sampled * image is 0.5 per sample, but the resampling reduces this. * * 3) Use a better filter with a super-sampled image; in the limit this is the * sinc() approach. * * 4) Do the geometric calculation; a bivariate definite integral across the * shape, unfortunately this means evaluating Si(x), the integral of sinc(x), * which is still a lot of math. * * This code uses approach (3) with a bi-cubic filter and 8x super-sampling * and method (1) for the super-samples. This means that the sample is either * 0 or 1, depending on whether the sub-pixel is within or outside the shape. * The bi-cubic weights are also fixed and the 16 required weights are * pre-computed here (note that the 'scale' setting will need to be changed if * 'super' is increased). * * The code also calculates a sum to the edge of the filter. This is not * currently used by could be used to optimize the calculation. */ #if 0 /* bc code */ scale=10 super=8 define bicubic(x) { if (x <= 1) return (1.5*x - 2.5)*x*x + 1; if (x < 2) return (((2.5 - 0.5*x)*x - 4)*x + 2); return 0; } define sum(x) { auto s; s = 0; while (x < 2*super) { s = s + bicubic(x/super); x = x + 1; } return s; } define results(x) { auto b, s; b = bicubic(x/super); s = sum(x); print " /*", x, "*/ { ", b, ", ", s, " }"; return 1; } x=0 while (x<2*super) { x = x + results(x) if (x < 2*super) print "," print "\n" } quit #endif #define BICUBIC1(x) /* |x| <= 1 */ ((1.5*(x)* - 2.5)*(x)*(x) + 1) #define BICUBIC2(x) /* 1 < |x| < 2 */ (((2.5 - 0.5*(x))*(x) - 4)*(x) + 2) #define FILTER_WEIGHT 9 /* Twice the first sum below */ #define FILTER_WIDTH 2 /* Actually half the width; -2..+2 */ #define FILTER_STEPS 8 /* steps per filter unit */ static const double bicubic[16][2] = { /* These numbers are exact; the weight for the filter is 1/9, but this * would make the numbers inexact, so it is not included here. */ /* bicubic sum */ /* 0*/ { 1.0000000000, 4.5000000000 }, /* 1*/ { .9638671875, 3.5000000000 }, /* 2*/ { .8671875000, 2.5361328125 }, /* 3*/ { .7275390625, 1.6689453125 }, /* 4*/ { .5625000000, .9414062500 }, /* 5*/ { .3896484375, .3789062500 }, /* 6*/ { .2265625000, -.0107421875 }, /* 7*/ { .0908203125, -.2373046875 }, /* 8*/ { 0, -.3281250000 }, /* 9*/ { -.0478515625, -.3281250000 }, /*10*/ { -.0703125000, -.2802734375 }, /*11*/ { -.0732421875, -.2099609375 }, /*12*/ { -.0625000000, -.1367187500 }, /*13*/ { -.0439453125, -.0742187500 }, /*14*/ { -.0234375000, -.0302734375 }, /*15*/ { -.0068359375, -.0068359375 } }; static double alpha_calc(const struct arg *arg, double x, double y) { /* For [x-2..x+2],[y-2,y+2] calculate the weighted bicubic given a function * which tells us whether a point is inside or outside the shape. First * check if we need to do this at all: */ switch (arg->check_fn(arg, x, y)) { case OUTSIDE: return 0; /* all samples outside the shape */ case INSIDE: return 1; /* all samples inside the shape */ default: { int dy; double alpha = 0; # define FILTER_D (FILTER_WIDTH*FILTER_STEPS-1) for (dy=-FILTER_D; dy<=FILTER_D; ++dy) { double wy = bicubic[abs(dy)][0]; if (wy != 0) { double alphay = 0; int dx; for (dx=-FILTER_D; dx<=FILTER_D; ++dx) { double wx = bicubic[abs(dx)][0]; if (wx != 0 && arg->inside_fn(arg, x+dx/16, y+dy/16)) alphay += wx; } alpha += wy * alphay; } } /* This needs to be weighted for each dimension: */ return alpha / (FILTER_WEIGHT*FILTER_WEIGHT); } } } /* These are the shape functions. */ /* "square", * { inside_square_filled, check_square_filled }, * { inside_square, check_square } */ static int square_check(double x, double y, double x1, double y1, double x2, double y2) /* Is x,y inside the square (x1,y1)..(x2,y2)? */ { /* Do a modified Cohen-Sutherland on one point, bit patterns that indicate * 'outside' are: * * x<x1 | x<y1 | x<x2 | x<y2 * 0 x 0 x To the right * 1 x 1 x To the left * x 0 x 0 Below * x 1 x 1 Above * * So 'inside' is (x<x1) != (x<x2) && (y<y1) != (y<y2); */ return ((x<x1) ^ (x<x2)) & ((y<y1) ^ (y<y2)); } static int inside_square_filled(const struct arg *arg, double x, double y) { return square_check(x, y, arg->x1, arg->y1, arg->x2, arg->y2); } static int square_check_line(const struct arg *arg, double x, double y, double w) /* Check for a point being inside the boundaries implied by the given arg * and assuming a width 2*w each side of the boundaries. This returns the * 'check' INSIDE/OUTSIDE/0 result but note the semantics: * * +--------------+ * | | OUTSIDE * | INSIDE | * | | * +--------------+ * * And '0' means within the line boundaries. */ { double cx = (arg->x1+arg->x2)/2; double wx = fabs(arg->x1-arg->x2)/2; double cy = (arg->y1+arg->y2)/2; double wy = fabs(arg->y1-arg->y2)/2; if (square_check(x, y, cx-wx-w, cy-wy-w, cx+wx+w, cy+wy+w)) { /* Inside, but maybe too far; check for the redundant case where * the lines overlap: */ wx -= w; wy -= w; if (wx > 0 && wy > 0 && square_check(x, y, cx-wx, cy-wy, cx+wx, cy+wy)) return INSIDE; /* between (inside) the boundary lines. */ return 0; /* inside the lines themselves. */ } return OUTSIDE; /* outside the boundary lines. */ } static int check_square_filled(const struct arg *arg, double x, double y) { /* The filter extends +/-FILTER_WIDTH each side of each output point, so * the check has to expand and contract the square by that amount; '0' * means close enough to the edge of the square that the bicubic filter has * to be run, OUTSIDE means alpha==0, INSIDE means alpha==1. */ return square_check_line(arg, x, y, FILTER_WIDTH); } static int inside_square(const struct arg *arg, double x, double y) { /* Return true if within the drawn lines, else false, no need to distinguish * INSIDE vs OUTSIDE here: */ return square_check_line(arg, x, y, arg->width/2) == 0; } static int check_square(const struct arg *arg, double x, double y) { /* So for this function a result of 'INSIDE' means inside the actual lines. */ double w = arg->width/2; if (square_check_line(arg, x, y, w+FILTER_WIDTH) == 0) { /* Somewhere close to the boundary lines. If far enough inside one of * them then we can return INSIDE: */ w -= FILTER_WIDTH; if (w > 0 && square_check_line(arg, x, y, w) == 0) return INSIDE; /* Point is somewhere in the filter region: */ return 0; } else /* Inside or outside the square by more than w+FILTER_WIDTH. */ return OUTSIDE; } /* "circle", * { inside_circle_filled, check_circle_filled }, * { inside_circle, check_circle } * * The functions here are analoguous to the square ones; however, they check * the corresponding ellipse as opposed to the rectangle. */ static int circle_check(double x, double y, double x1, double y1, double x2, double y2) { if (square_check(x, y, x1, y1, x2, y2)) { /* Inside the square, so maybe inside the circle too: */ const double cx = (x1 + x2)/2; const double cy = (y1 + y2)/2; const double dx = x1 - x2; const double dy = y1 - y2; x = (x - cx)/dx; y = (y - cy)/dy; /* It is outside if the distance from the center is more than half the * diameter: */ return x*x+y*y < .25; } return 0; /* outside */ } static int inside_circle_filled(const struct arg *arg, double x, double y) { return circle_check(x, y, arg->x1, arg->y1, arg->x2, arg->y2); } static int circle_check_line(const struct arg *arg, double x, double y, double w) /* Check for a point being inside the boundaries implied by the given arg * and assuming a width 2*w each side of the boundaries. This function has * the same semantic as square_check_line but tests the circle. */ { double cx = (arg->x1+arg->x2)/2; double wx = fabs(arg->x1-arg->x2)/2; double cy = (arg->y1+arg->y2)/2; double wy = fabs(arg->y1-arg->y2)/2; if (circle_check(x, y, cx-wx-w, cy-wy-w, cx+wx+w, cy+wy+w)) { /* Inside, but maybe too far; check for the redundant case where * the lines overlap: */ wx -= w; wy -= w; if (wx > 0 && wy > 0 && circle_check(x, y, cx-wx, cy-wy, cx+wx, cy+wy)) return INSIDE; /* between (inside) the boundary lines. */ return 0; /* inside the lines themselves. */ } return OUTSIDE; /* outside the boundary lines. */ } static int check_circle_filled(const struct arg *arg, double x, double y) { return circle_check_line(arg, x, y, FILTER_WIDTH); } static int inside_circle(const struct arg *arg, double x, double y) { return circle_check_line(arg, x, y, arg->width/2) == 0; } static int check_circle(const struct arg *arg, double x, double y) { /* Exactly as the 'square' code. */ double w = arg->width/2; if (circle_check_line(arg, x, y, w+FILTER_WIDTH) == 0) { w -= FILTER_WIDTH; if (w > 0 && circle_check_line(arg, x, y, w) == 0) return INSIDE; /* Point is somewhere in the filter region: */ return 0; } else /* Inside or outside the square by more than w+FILTER_WIDTH. */ return OUTSIDE; } /* "line", * { NULL, NULL }, There is no 'filled' line. * { inside_line, check_line } */ static int line_check(double x, double y, double x1, double y1, double x2, double y2, double w, double expand) { /* Shift all the points to (arg->x1, arg->y1) */ double lx = x2 - x1; double ly = y2 - y1; double len2 = lx*lx + ly*ly; double cross, dot; x -= x1; y -= y1; /* The dot product is the distance down the line, the cross product is * the distance away from the line: * * distance = |cross| / sqrt(len2) */ cross = x * ly - y * lx; /* If 'distance' is more than w the point is definitely outside the line: * * distance >= w * |cross| >= w * sqrt(len2) * cross^2 >= w^2 * len2: */ if (cross*cross >= (w+expand)*(w+expand)*len2) return 0; /* outside */ /* Now find the distance *along* the line; this comes from the dot product * lx.x+ly.y. The actual distance (in pixels) is: * * distance = dot / sqrt(len2) */ dot = lx * x + ly * y; /* The test for 'outside' is: * * distance < 0 || distance > sqrt(len2) * -> dot / sqrt(len2) > sqrt(len2) * -> dot > len2 * * But 'expand' is used for the filter width and needs to be handled too: */ return dot > -expand && dot < len2+expand; } static int inside_line(const struct arg *arg, double x, double y) { return line_check(x, y, arg->x1, arg->y1, arg->x2, arg->y2, arg->width/2, 0); } static int check_line(const struct arg *arg, double x, double y) { /* The end caps of the line must be checked too; it's not enough just to * widen the line by FILTER_WIDTH; 'expand' exists for this purpose: */ if (line_check(x, y, arg->x1, arg->y1, arg->x2, arg->y2, arg->width/2, FILTER_WIDTH)) { /* Inside the line+filter; far enough inside that the filter isn't * required? */ if (arg->width > 2*FILTER_WIDTH && line_check(x, y, arg->x1, arg->y1, arg->x2, arg->y2, arg->width/2, -FILTER_WIDTH)) return INSIDE; return 0; } return OUTSIDE; } static const struct { const char *name; shape_fn_ptr function[2/*fill,line*/][2]; # define FN_INSIDE 0 # define FN_CHECK 1 } shape_defs[] = { { "square", { { inside_square_filled, check_square_filled }, { inside_square, check_square } } }, { "circle", { { inside_circle_filled, check_circle_filled }, { inside_circle, check_circle } } }, { "line", { { NULL, NULL }, { inside_line, check_line } } } }; #define shape_count ((sizeof shape_defs)/(sizeof shape_defs[0])) static shape_fn_ptr shape_of(const char *arg, double width, int f) { unsigned int i; for (i=0; i<shape_count; ++i) if (strcmp(shape_defs[i].name, arg) == 0) { shape_fn_ptr fn = shape_defs[i].function[width != 0][f]; if (fn != NULL) return fn; fprintf(stderr, "genpng: %s %s not supported\n", width == 0 ? "filled" : "unfilled", arg); exit(1); } fprintf(stderr, "genpng: %s: not a valid shape name\n", arg); exit(1); } static void parse_arg(struct arg *arg, const char **argv/*7 arguments*/) { /* shape ::= color width shape x1 y1 x2 y2 */ arg->color = color_of(argv[0]); arg->width = width_of(argv[1]); arg->inside_fn = shape_of(argv[2], arg->width, FN_INSIDE); arg->check_fn = shape_of(argv[2], arg->width, FN_CHECK); arg->x1 = coordinate_of(argv[3]); arg->y1 = coordinate_of(argv[4]); arg->x2 = coordinate_of(argv[5]); arg->y2 = coordinate_of(argv[6]); } static png_uint_32 read_wh(const char *name, const char *str) /* read a PNG width or height */ { char *ep = NULL; unsigned long ul = strtoul(str, &ep, 10); if (ep != NULL && *ep == 0 && ul > 0 && ul <= 0x7fffffff) return (png_uint_32)/*SAFE*/ul; fprintf(stderr, "genpng: %s: invalid number %s\n", name, str); exit(1); } static void pixel(png_uint_16p p, struct arg *args, int nargs, double x, double y) { /* Fill in the pixel by checking each shape (args[nargs]) for effects on * the corresponding sample: */ double r=0, g=0, b=0, a=0; while (--nargs >= 0 && a != 1) { /* NOTE: alpha_calc can return a value outside the range 0..1 with the * bicubic filter. */ const double alpha = alpha_calc(args+nargs, x, y) * (1-a); r += alpha * args[nargs].color->red; g += alpha * args[nargs].color->green; b += alpha * args[nargs].color->blue; a += alpha; } /* 'a' may be negative or greater than 1; if it is, negative clamp the * pixel to 0 if >1 clamp r/g/b: */ if (a > 0) { if (a > 1) { if (r > 1) r = 1; if (g > 1) g = 1; if (b > 1) b = 1; a = 1; } /* And fill in the pixel: */ p[0] = (png_uint_16)/*SAFE*/round(r * 65535); p[1] = (png_uint_16)/*SAFE*/round(g * 65535); p[2] = (png_uint_16)/*SAFE*/round(b * 65535); p[3] = (png_uint_16)/*SAFE*/round(a * 65535); } else p[3] = p[2] = p[1] = p[0] = 0; } int main(int argc, const char **argv) { int convert_to_8bit = 0; /* There is one option: --8bit: */ if (argc > 1 && strcmp(argv[1], "--8bit") == 0) --argc, ++argv, convert_to_8bit = 1; if (argc >= 3) { png_uint_16p buffer; int nshapes; png_image image; # define max_shapes 256 struct arg arg_list[max_shapes]; /* The libpng Simplified API write code requires a fully initialized * structure. */ memset(&image, 0, sizeof image); image.version = PNG_IMAGE_VERSION; image.opaque = NULL; image.width = read_wh("width", argv[1]); image.height = read_wh("height", argv[2]); image.format = PNG_FORMAT_LINEAR_RGB_ALPHA; image.flags = 0; image.colormap_entries = 0; /* Check the remainder of the arguments */ for (nshapes=0; 3+7*(nshapes+1) <= argc && nshapes < max_shapes; ++nshapes) parse_arg(arg_list+nshapes, argv+3+7*nshapes); if (3+7*nshapes != argc) { fprintf(stderr, "genpng: %s: too many arguments\n", argv[3+7*nshapes]); return 1; } /* Create the buffer: */ buffer = malloc(PNG_IMAGE_SIZE(image)); if (buffer != NULL) { png_uint_32 y; /* Write each row... */ for (y=0; y<image.height; ++y) { png_uint_32 x; /* Each pixel in each row: */ for (x=0; x<image.width; ++x) pixel(buffer + 4*(x + y*image.width), arg_list, nshapes, x, y); } /* Write the result (to stdout) */ if (png_image_write_to_stdio(&image, stdout, convert_to_8bit, buffer, 0/*row_stride*/, NULL/*colormap*/)) { free(buffer); return 0; /* success */ } else fprintf(stderr, "genpng: write stdout: %s\n", image.message); free(buffer); } else fprintf(stderr, "genpng: out of memory: %lu bytes\n", (unsigned long)PNG_IMAGE_SIZE(image)); } else { /* Wrong number of arguments */ fprintf(stderr, "genpng: usage: genpng [--8bit] width height {shape}\n" " Generate a transparent PNG in RGBA (truecolor+alpha) format\n" " containing the given shape or shapes. Shapes are defined:\n" "\n" " shape ::= color width shape x1 y1 x2 y2\n" " color ::= black|white|red|green|yellow|blue\n" " color ::= brown|purple|pink|orange|gray|cyan\n" " width ::= filled|<number>\n" " shape ::= circle|square|line\n" " x1,x2 ::= <number>\n" " y1,y2 ::= <number>\n" "\n" " Numbers are floating point numbers describing points relative to\n" " the top left of the output PNG as pixel coordinates. The 'width'\n" " parameter is either the width of the line (in output pixels) used\n" " to draw the shape or 'filled' to indicate that the shape should\n" " be filled with the color.\n" "\n" " Colors are interpreted loosely to give access to the eight full\n" " intensity RGB values:\n" "\n" " black, red, green, blue, yellow, cyan, purple, white,\n" "\n" " Cyan is full intensity blue+green; RGB(0,1,1), plus the following\n" " lower intensity values:\n" "\n" " brown: red+orange: RGB(0.5, 0.125, 0) (dark red+orange)\n" " pink: red+white: RGB(1.0, 0.5, 0.5)\n" " orange: red+yellow: RGB(1.0, 0.5, 0)\n" " gray: black+white: RGB(0.5, 0.5, 0.5)\n" "\n" " The RGB values are selected to make detection of aliasing errors\n" " easy. The names are selected to make the description of errors\n" " easy.\n" "\n" " The PNG is written to stdout, if --8bit is given a 32bpp RGBA sRGB\n" " file is produced, otherwise a 64bpp RGBA linear encoded file is\n" " written.\n"); } return 1; } #endif /* SIMPLIFIED_WRITE && STDIO */