/*
 *  PHP extension - Maz Froumentin's Textorizer 
 *
 *  Paul Downey, paul.downey@whatfettle.com
 *
 *  http://textorizer.whatfettle.com
 *
 ** textorizer.c: vectorises a picture into an SVG using text strings
 ** see: http://www.w3.org/People/maxf/textorizer/ 
 ** Copyright Max Froumentin 2005
 ** This software is distributed under the 
 ** W3C® SOFTWARE NOTICE AND LICENSE
 ** http://www.w3.org/Consortium/Legal/2002/copyright-software-20021231
 *
 */

#ifdef HAVE_CONFIG_H
#include "config.h"
#endif

#include <png.h>

#include "php.h"
#include "php_ini.h"
#include "ext/standard/info.h"
#include "ext/standard/php_smart_str.h"
#include "php_textorize.h"

#include <strings.h>
#include <stdlib.h>
#include <assert.h>
#include <math.h>

#define mmalloc malloc
#define mfree free

/*
 *  maximum number of lines of text
 */
#define MAX_WORDS 1024

/*
 *  individual pixel to be processed
 *  - rather greedy on memory
 */
struct pixel
{
    unsigned char   r;
    unsigned char   g;
    unsigned char   b;
    unsigned char   a;
};

/*
 *  collection of pixels
 */
struct pixel_map
{
    long width;
    long height;
    struct pixel *pixels;
};

/*
 *  PNG signature
 */
#define PNG_SIG_LEN 8

#ifndef png_jmpbuf
#   define png_jmpbuf(png) ((png)->jmpbuf)
#endif

typedef struct png_buff
{
    const unsigned char *buffer;
    int pos;
} 
png_buff_t;

static void png_read_buff(png_struct *png, png_bytep buf, png_size_t size)
{
png_buff_t *png_buff = (png_buff_t *)png_get_io_ptr(png);

    memcpy(buf, png_buff->buffer+png_buff->pos,size);
    png_buff->pos += size;
}

static int loadpng(const void *buf, size_t len, struct pixel_map *map)
{
png_structp png;
png_infop info;
png_uint_32 width;
png_uint_32 height;
png_bytep *rowp;
png_buff_t png_buff;
int bits;
int colors;
unsigned long   i, j, k;


    map->width = map->height = 0;

    png_buff.pos = 0;
    png_buff.buffer = buf;

    /* 
     *  create the png data structures
     */
    png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);

    if (!png)
        return -1;

    info = png_create_info_struct(png);

    if (!info)
    {
        png_destroy_read_struct(&png, (png_infopp) NULL, (png_infopp) NULL);
        return -1;
    }

    if (setjmp(png_jmpbuf(png)))
    {
        png_destroy_read_struct(&png, &info, (png_infopp) NULL);
        return -1;
    }

    png_set_read_fn(png, (png_voidp)&png_buff, (png_rw_ptr)png_read_buff);

    png_read_info(png, info);

    png_get_IHDR(png, info, &width, &height, &bits, &colors, NULL, NULL, NULL);

    /* 
     *  tell libpng to strip 16 bit/color files down to 8 bits/color 
     */
    png_set_strip_16(png);

    /*
     * Extract multiple pixels with bit depths of 1, 2, and 4 from a
     * single byte into separate bytes (useful for paletted and grayscale images).
     */
    png_set_packing(png);

    /*
     * Expand grayscale images to the full 8 bits from 1, 2, or 4
     * bits/pixel
     */
    if (colors == PNG_COLOR_TYPE_GRAY && bits < 8)
        png_set_expand(png);

    /* 
     * expand paletted colors into true RGB triplets 
     */
    if (colors == PNG_COLOR_TYPE_PALETTE)
        png_set_expand(png);

    /*
     * Expand paletted or RGB images with transparency to full alpha
     * channels so the data will be available as RGBA quartets.
     */
    if (png_get_valid(png, info, PNG_INFO_tRNS))
        png_set_expand(png);

    /*
     * (colors == PNG_COLOR_TYPE_RGB||colors == PNG_COLOR_TYPE_GRAY))
     */
    png_set_filler(png, 0x000000ff, PNG_FILLER_AFTER);

    /*
     * update info from the transforms. used to get a correct value
     * for png_get_rowbytes
     */
    png_read_update_info(png, info);

    /* 
     * allocate the memory to hold a row of the image
     */
    rowp = (png_bytep *) mmalloc((height * sizeof(png_bytep)));

    for (i = 0; i < height; i++)
        rowp[i] = mmalloc(png_get_rowbytes(png, info));

    png_read_image(png, rowp);

    /* 
     *  malloc pixmap data
     *  - likely to fail on memory constrianed servers
     */
    map->width = width;
    map->height = height;
    map->pixels = mmalloc((size_t)width 
                    * (size_t)height 
                    * sizeof(struct pixel));

    if (NULL == map->pixels)
    {
        php_printf("malloc failed");
        return -1;
    }

    k = 0;
    for (i = 0; i < height; i++)
        for (j = 0; j < width; j++)
        {
            map->pixels[k].r = rowp[i][4 * j];
            map->pixels[k].g = rowp[i][4 * j + 1];
            map->pixels[k++].b = rowp[i][4 * j + 2];
        }

    /*
     *  libpng needs to see the end of file
     */
    png_read_end(png, info);

    /*
     *  cleanup
     */
    png_destroy_read_struct(&png, &info, (png_infopp) NULL);

    for (i = 0; i < height; i++)
        mfree(rowp[i]);

    mfree(rowp);

    return 0;
}


/* 
 *  Sobel convolution filter
 *  http://en.wikipedia.org/wiki/Sobel
 */
static const long Sx[3][3] = 
{
    { -1,  0,  1 }, 
    { -2,  0,  2 },
    { -1,  0,  1 }
};

static const long Sy[3][3] = 
{
    { -1, -2, -1 },
    {  0,  0,  0 },
    {  1,  2,  1 }
};

#define rAt(x, y)  (pixels[y * width + x].r)
#define gAt(x, y)  (pixels[y * width + x].g)
#define bAt(x, y)  (pixels[y * width + x].b)

static int pixelAt(const struct pixel_map *map, int x, int y)
{
    if (x < 0)
        x = 0;

    if (x >= map->width)
        x = map->width - 1;

    if (y < 0)
        y = 0;

    if (y >= map->height)
        y = map->height - 1;

    return (int) rint((
        map->pixels[y * map->width + x].r 
        + map->pixels[y * map->width + x].g 
        + map->pixels[y * map->width + x].b) / 3);
}


static int make_svg(
    struct pixel_map *map, 
    const char *words[], 
    long nwords,
    long nstrokes, 
    long threshold,
    long swidth,
    long sheight)
{
unsigned char  *randomX;
unsigned char  *randomY;
int  h, i, j, x, y, v;
float dir, dx, dy;
const char *word;
struct pixel *pixels = map->pixels;
long width = map->width;
long height = map->height;
float dmag2;
float B;
float scale;

    php_printf("<?xml version='1.0'?>\n");
    php_printf("<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 20010904//EN' \n");
    php_printf("  'http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd'>\n");
    php_printf("<svg width='%ldpx' height='%ldpx' viewBox='0 0 %ld %ld'\n", 
                        swidth, sheight,
                        width, height);
    php_printf("     xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'>\n");
    php_printf("  <desc>Textorized</desc>\n");
    php_printf("  <g font-family='Verdana' font-weight='bold' font-size='10' text-anchor='middle' fill='black'>\n");

    php_printf("  <!-- %ld %ld %ld %ld -->\n", nstrokes, threshold, swidth, sheight);

    for (h = 0; h < nstrokes;)
    {
        x = rand() % width;
        y = rand() % height;

        v = pixelAt(map, x, y);

        if (v != 255)
        {
            /* compute pixel derivative */
            dx = dy = 0;
            for (i = 0; i < 3; i++)
                for (j = 0; j < 3; j++)
                {
                    int vnear = pixelAt(map, x + i - 1, y + j - 1);
                    dx += Sx[j][i] * vnear;
                    dy += Sy[j][i] * vnear;
                }

            dx /= 8;
            dy /= 8;

            dmag2 = dx * dx + dy * dy;

            /* 
             * arbitrary threshold on gradient 
             */
            if (dmag2 > threshold)
            {
                h++;

                B = 2 * (width + height) / 5000.;
                scale = B + B * sqrt(dmag2) / 100;

                if (dx == 0)
                    dir = M_PI / 2;
                else if (dx > 0)
                    dir = atan(dy / dx);
                else if (dy == 0)
                    dir = 0;
                else if (dy > 0)
                    dir = atan(-dx / dy) + M_PI / 2;
                else
                    dir = atan(dy / dx) + M_PI;

                word = words[h % nwords];

                php_printf("<text transform='translate(%d,%d) rotate(%d) scale(%f)' fill='#%02x%02x%02x'>%s</text>\n",
                         x, y, 
                        (int) rint(dir * 360 / 6.28) + 90, 
                        scale, 
                        rAt(x, y), gAt(x, y), bAt(x, y), 
                        word);
            }
        }
    }

    php_printf("  </g>\n");
    php_printf("</svg>\n");

    return 0;
}

#ifdef COMPILE_DL_TEXTORIZE
    ZEND_GET_MODULE( textorize )
#endif

PHP_MINIT_FUNCTION( textorize )
{
    return SUCCESS;
}

PHP_RINIT_FUNCTION( textorize )
{
    return SUCCESS;
}

PHP_RSHUTDOWN_FUNCTION( textorize )
{
    return SUCCESS;
}

PHP_MINFO_FUNCTION( textorize )
{
    php_info_print_table_start();
    php_info_print_table_header(2, "textorize support", "enabled");
    php_info_print_table_end();
}


PHP_FUNCTION( textorize )
{
struct pixel_map map = { 0 };
char *blob = 0;
int blen = 0;
char *text = 0;
long tlen = 0;
char *words[MAX_WORDS];
long nwords = 0;
long nstrokes = 1000;
long threshold = 10;
long swidth = 800;
long sheight = 600;

    if (zend_parse_parameters( ZEND_NUM_ARGS() TSRMLS_CC, "ssllll", 
                    &blob, &blen,
                    &text, &tlen,
                    &nstrokes,
                    &threshold,
                    &swidth,
                    &sheight) == FAILURE)
        return;

    /*
     *  split string
     */
    {
        const char *p = text;
        int n;
        int seg = 0;

        for (n = 0; n < MAX_WORDS; n++)
        {
            p += seg;
            p += strspn(p, "\n\r");

            if (0 == (seg = strcspn(p, "\n\r")))
                break;

            words[nwords] = mmalloc(seg+1);
            strncpy(words[nwords], p, seg);
            words[nwords][seg] = 0;
            nwords++;
        }

        if (!nwords)
            return;
    }

    if (loadpng(blob, blen, &map))
        return;

    make_svg(&map, (void *)words, nwords, nstrokes, threshold, swidth, sheight);

    return;
}

zend_function_entry textorize_functions[] =
{
        PHP_FE(textorize, NULL)

        { NULL, NULL, NULL }
};

zend_module_entry textorize_module_entry =
{
        STANDARD_MODULE_HEADER,
        "textorize",
        textorize_functions,
        PHP_MINIT( textorize ), 
        NULL,
        PHP_RINIT( textorize ),
        PHP_RSHUTDOWN( textorize ),
        PHP_MINFO( textorize ),
        TEXTORIZE_VERSION,
        STANDARD_MODULE_PROPERTIES
};
