/*
 * Copyright (c) 2006 Kazuaki ODA.
 * Copyright (c) 2006 Mode2 inc.
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 * 3. Neither the name of the company nor the names of its contributors
 *    may be used to endorse or promote products derived from this software
 *    without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * $Id: ctr_img_support.c 436 2006-11-11 08:52:58Z oda $
 */

#include <stdio.h>
#include <stdlib.h>
#include "wand/magick_wand.h"
#undef PACKAGE_BUGREPORT
#undef PACKAGE_NAME
#undef PACKAGE_STRING
#undef PACKAGE_TARNAME
#undef PACKAGE_VERSION
#include "httpd.h"
#include "http_log.h"
#include "apr_date.h"
#include "apr_strings.h"
#include "ctr_img_support.h"
#include "ctr_util.h"

/*
 * The request to an image, which will be converted, have a mod_cthree
 * specific parameter "_c3i" in a query string.  The type of the parameter
 * value is 32-bit integer, and several information is stored in that value.
 * The following explains what does each bit of the value means:
 *
 * RRRRRRRR FFFFFFFF FFCPPPPP PPMMMSSS
 * S - original suffix (1:gif, 2:jpg, 3:jpeg, 4:png)
 * M - conversion mode
 * P - what percentage of screen width the image width is
 * C - can attach the image on E-mail or not
 * F - main frame number of animation GIF
 * R - reserved
 */

#define CTR_IMG_SUFFIX_NBITS            3
#define CTR_IMG_SUFFIX_SHIFT            0
#define CTR_IMG_SUFFIX_MASK             ((1 << CTR_IMG_SUFFIX_NBITS) - 1)
#define CTR_IMG_SUFFIX_MASK2            \
    (CTR_IMG_SUFFIX_MASK << CTR_IMG_SUFFIX_SHIFT)
#define CTR_IMG_SUFFIX_GET(n)                                           \
    (ctr_img_num_to_suffix(((n) & CTR_IMG_SUFFIX_MASK2)                 \
                           >> CTR_IMG_SUFFIX_SHIFT))
#define CTR_IMG_SUFFIX_SET(n, suffix)                                   \
    ((n) |= (ctr_img_suffix_to_num(suffix) & CTR_IMG_SUFFIX_MASK)       \
     << CTR_IMG_SUFFIX_SHIFT)

#define CTR_IMG_CONVMODE_NBITS          3
#define CTR_IMG_CONVMODE_SHIFT          \
    (CTR_IMG_SUFFIX_SHIFT + CTR_IMG_SUFFIX_NBITS)
#define CTR_IMG_CONVMODE_MASK           ((1 << CTR_IMG_CONVMODE_NBITS) - 1)
#define CTR_IMG_CONVMODE_MASK2          \
    (CTR_IMG_CONVMODE_MASK << CTR_IMG_CONVMODE_SHIFT)
#define CTR_IMG_CONVMODE_GET(n)                                         \
    (((uint32_t)(n) & CTR_IMG_CONVMODE_MASK2) >> CTR_IMG_CONVMODE_SHIFT)
#define CTR_IMG_CONVMODE_SET(n, mode)                                   \
    ((n) |= ((uint32_t)(mode) & CTR_IMG_CONVMODE_MASK)                  \
     << CTR_IMG_CONVMODE_SHIFT)

#define CTR_IMG_PERCENT_NBITS           7
#define CTR_IMG_PERCENT_SHIFT           \
    (CTR_IMG_CONVMODE_SHIFT + CTR_IMG_CONVMODE_NBITS)
#define CTR_IMG_PERCENT_MASK            ((1 << CTR_IMG_PERCENT_NBITS) - 1)
#define CTR_IMG_PERCENT_MASK2           \
    (CTR_IMG_PERCENT_MASK << CTR_IMG_PERCENT_SHIFT)
#define CTR_IMG_PERCENT_GET(n)                                          \
    (((uint32_t)(n) & CTR_IMG_PERCENT_MASK2) >> CTR_IMG_PERCENT_SHIFT)
#define CTR_IMG_PERCENT_SET(n, percent)                                 \
    ((n) |= ((uint32_t)(percent) & CTR_IMG_PERCENT_MASK)                \
     << CTR_IMG_PERCENT_SHIFT)

#define CTR_IMG_COPY_NBITS              1
#define CTR_IMG_COPY_SHIFT              \
    (CTR_IMG_PERCENT_SHIFT + CTR_IMG_PERCENT_NBITS)
#define CTR_IMG_COPY_MASK               ((1 << CTR_IMG_COPY_NBITS) - 1)
#define CTR_IMG_COPY_MASK2              \
    (CTR_IMG_COPY_MASK << CTR_IMG_COPY_SHIFT)
#define CTR_IMG_COPY_GET(n)                                             \
    (((uint32_t)(n) & CTR_IMG_COPY_MASK2) >> CTR_IMG_COPY_SHIFT)
#define CTR_IMG_COPY_SET(n, copy)                                       \
    ((n) |= ((uint32_t)(copy) & CTR_IMG_COPY_MASK) << CTR_IMG_COPY_SHIFT)

#define CTR_IMG_MAINFRAME_NBITS         10
#define CTR_IMG_MAINFRAME_SHIFT         \
    (CTR_IMG_COPY_SHIFT + CTR_IMG_COPY_NBITS)
#define CTR_IMG_MAINFRAME_MASK          ((1 << CTR_IMG_MAINFRAME_NBITS) - 1)
#define CTR_IMG_MAINFRAME_MASK2         \
    (CTR_IMG_MAINFRAME_MASK << CTR_IMG_MAINFRAME_SHIFT)
#define CTR_IMG_MAINFRAME_GET(n)                                        \
    (((uint32_t)(n) & CTR_IMG_MAINFRAME_MASK2) >> CTR_IMG_MAINFRAME_SHIFT)
#define CTR_IMG_MAINFRAME_SET(n, mframe)                                \
    ((n) |= ((uint32_t)(mframe) & CTR_IMG_MAINFRAME_MASK)               \
     << CTR_IMG_MAINFRAME_SHIFT)

#define CTR_IMG_PARAM                   "_c3i"

#define CTR_IMG_DSTFMT_HEADER           "X-C3-Img-DstFmt"
#define CTR_IMG_CONVMODE_HEADER         "X-C3-Img-ConvMode"
#define CTR_IMG_PERCENT_HEADER          "X-C3-Img-Percent"
#define CTR_IMG_COPY_HEADER             "X-C3-Img-Copy"
#define CTR_IMG_MAINFRAME_HEADER        "X-C3-Img-MainFrame"

static uint32_t crc[256];
static const char *img_suffixes[] = {
    "", "gif", "jpg", "jpeg", "png"
};

static uint32_t ctr_img_suffix_to_num(const char *suffix);
static const char *ctr_img_num_to_suffix(uint32_t n);
static long ctr_imgmax_calc(const char *smax, const ctr_browser_t *brw,
                            apr_pool_t *pool);
static char *ctr_imgdir_get(const char *basedir, const request_rec *r);
static char *ctr_imgfname_get(const char *dir, const ctr_browser_t *brw,
                              ctr_convmode_t cvmaode, int percent, int copy,
                              int mframe, long maxsize, const char *suffix,
                              apr_pool_t *pool);
static int ctr_img_scale(MagickWand *wand, ctr_convmode_t cvmode,
                         double percent, ctr_picformat_t fmt,
                         const ctr_browser_t *brw);
static int ctr_imglayers_optimize(MagickWand *wand);
static apr_bucket *ctr_bucket_create_from_imgfile(apr_file_t *fd,
                                                  apr_time_t last_modified,
                                                  const request_rec *r);
static int ctr_pictype_search(const ctr_pictype_t *pt, ctr_picformat_t fmt);
static int ctr_png_comment(const char *in, apr_file_t *out, const char *key,
                           const char *val, apr_pool_t *pool);

/* ARGSUSED */
void ctr_img_child_init(apr_pool_t *pool, server_rec *server)
{
    uint32_t c;
    int i, j;

#if MagickLibVersion >= 0x625
    MagickWandGenesis();
#endif

    /*
     * Make CRC table for PNG Copyright comment.
     */
    for (i = 0; i < 256; i++) {
        c = (uint32_t)i;
        for (j = 0; j < 8; j++) {
            if (c & 1)
                c = 0xedb88320L ^ (c >> 1);
            else
                c = c >> 1;
        }
        crc[i] = c;
    }
}

void ctr_img_post_read_request(request_rec *r)
{
    apr_pool_t *pool = r->pool;
    const char *p, *tkn;
    char *newargs, *newtkn;

    if (r->args == NULL)
        return;

    p = apr_pstrdup(pool, r->args);
    newargs = NULL;

    while (*p && (tkn = ap_getword(pool, &p, '&'))) {
        char *key = ap_getword(pool, &tkn, '=');
        char *val = ap_getword(pool, &tkn, '=');

        if (strcmp(key, CTR_IMG_PARAM) == 0) {
            /*
             * Found a mod_cthree specific parameter for image conversion.
             */
            uint32_t ival = (uint32_t)apr_atoi64(val);
            const char *suffix;

            r->uri = ctr_uri_suffix_remove(r->uri, &suffix, pool);
            r->uri = apr_pstrcat(pool, r->uri, ".",
                                 CTR_IMG_SUFFIX_GET(ival), NULL);

            apr_table_setn(r->headers_in, CTR_IMG_DSTFMT_HEADER, suffix);
            apr_table_setn(r->headers_in, CTR_IMG_CONVMODE_HEADER,
                           apr_itoa(pool, CTR_IMG_CONVMODE_GET(ival)));
            apr_table_setn(r->headers_in, CTR_IMG_PERCENT_HEADER,
                           apr_itoa(pool, CTR_IMG_PERCENT_GET(ival)));
            apr_table_setn(r->headers_in, CTR_IMG_COPY_HEADER,
                           apr_itoa(pool, CTR_IMG_COPY_GET(ival)));
            apr_table_setn(r->headers_in, CTR_IMG_MAINFRAME_HEADER,
                           apr_itoa(pool, CTR_IMG_MAINFRAME_GET(ival)));
            continue;
        }

        newtkn = apr_pstrcat(pool, key, "=", val, NULL);
        if (newargs == NULL)
            newargs = newtkn;
        else
            newargs = apr_pstrcat(pool, newargs, "&", newtkn, NULL);
    }

    r->args = newargs;
}

void ctr_imguri_conv(const CTR_CLASS_NAME(image) *img,
                     const ctr_browser_t *brw, apr_pool_t *pool)
{
    apr_uri_t *uri;
    ctr_picformat_t fmt;
    char *sfmt;
    const char *suffix;
    uint32_t pval;

    if (img->src == NULL || img->cvmode == CV_NONE ||
        brw->carrier == CR_UNKNOWN)
        return;

    uri = img->src;

    /*
     * Determine image format.
     */
    if (ctr_caseendwith(uri->path, ".gif")) {
        /* GIF image */
        if (ctr_pictype_search(brw->pictypes, PC_GIF))
            fmt = PC_GIF;
        else if (ctr_pictype_search(brw->pictypes, PC_PNG256))
            fmt = PC_PNG256;
        else if (ctr_pictype_search(brw->pictypes, PC_JPEG))
            fmt = PC_JPEG;
        else if (ctr_pictype_search(brw->pictypes, PC_BMP4))
            fmt = PC_BMP4;
        else
            fmt = brw->pictypes->type;
    } else if (ctr_caseendwith(uri->path, ".jpg") ||
               ctr_caseendwith(uri->path, ".jpeg")) {
        /* JPEG image */
        if (ctr_pictype_search(brw->pictypes, PC_JPEG))
            fmt = PC_JPEG;
        else if (ctr_pictype_search(brw->pictypes, PC_GIF))
            fmt = PC_GIF;
        else if (ctr_pictype_search(brw->pictypes, PC_PNG256))
            fmt = PC_PNG256;
        else if (ctr_pictype_search(brw->pictypes, PC_BMP4))
            fmt = PC_BMP4;
        else
            fmt = brw->pictypes->type;
    } else if (ctr_caseendwith(uri->path, ".png")) {
        /* PNG image */
        if (ctr_pictype_search(brw->pictypes, PC_PNG256))
            fmt = PC_PNG256;
        else if (ctr_pictype_search(brw->pictypes, PC_GIF))
            fmt = PC_GIF;
        else if (ctr_pictype_search(brw->pictypes, PC_JPEG))
            fmt = PC_JPEG;
        else if (ctr_pictype_search(brw->pictypes, PC_BMP4))
            fmt = PC_BMP4;
        else
            fmt = brw->pictypes->type;
    } else
        return;

    switch (fmt) {
    case PC_GIF:
        sfmt = ".gif";
        break;
    case PC_PNG256:
        if (brw->carrier == CR_SOFTBANK && img->copy == FALSE)
            sfmt = ".pnz";
        else
            sfmt = ".png";
        break;
    case PC_BMP4:
        sfmt = ".bmp4";
        break;
    case PC_BMP2:
        sfmt = ".bmp2";
        break;
    case PC_JPEG:
    default:
        if (brw->carrier == CR_SOFTBANK && img->copy == FALSE)
            sfmt = ".jpz";
        else
            sfmt = ".jpg";
        break;
    }

    uri->path = ctr_uri_suffix_remove(uri->path, &suffix, pool);
    uri->path = apr_pstrcat(pool, uri->path, sfmt, NULL);
    pval = 0;
    CTR_IMG_SUFFIX_SET(pval, suffix);
    CTR_IMG_CONVMODE_SET(pval, img->cvmode);
    CTR_IMG_PERCENT_SET(pval, img->percent);
    CTR_IMG_COPY_SET(pval, img->copy);
    CTR_IMG_MAINFRAME_SET(pval, img->mframe);
    uri->query = apr_psprintf(pool, "%s%s%s=%d",
                              uri->query ? uri->query : "",
                              uri->query ? "&": "",
                              CTR_IMG_PARAM, pval);
}

apr_bucket *ctr_img_convert(const char *data, apr_size_t datalen,
                            const ctr_browser_t *brw, const char *max,
                            const char *cachedir, request_rec *r)
{
    apr_pool_t *pool = r->pool;
    conn_rec *conn = r->connection;
    apr_time_t last_modified;
    ctr_convmode_t cvmode;
    double percent;
    int copy;
    int mframe;
    ctr_picformat_t fmt;
    const char *suffix;
    int depth, colors, quality;
    long maxsize;
    char *sfmt;
    char *fdir, *fname, *tfname;
    const char *tdir;
    apr_file_t *fd, *tfd;
    apr_bucket *b;
    MagickWand *wand;
    int nimg;
    apr_finfo_t tfinfo;
    apr_size_t tfsize;
    char *buf;
    const char *s;
    apr_status_t rv;

    if ((s = apr_table_get(r->headers_out, "Last-Modified")) != NULL)
        last_modified = apr_date_parse_http(s);
    else
        last_modified = APR_DATE_BAD;

    if ((s = apr_table_get(r->headers_in, CTR_IMG_CONVMODE_HEADER)) == NULL) {
        /*
         * No need to convert image.
         */
        return apr_bucket_pool_create(data, datalen, pool, conn->bucket_alloc);
    }

    cvmode = (int)apr_atoi64(s);
    if (cvmode < CV_NORMAL || CV_KP < cvmode) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get image conversion mode");
        return NULL;
    }

    if (cvmode == CV_KP) {
        /*
         * Image has already been converted by an external software.
         */
        return apr_bucket_pool_create(data, datalen, pool, conn->bucket_alloc);
    }

    if ((s = apr_table_get(r->headers_in, CTR_IMG_PERCENT_HEADER)) == NULL ||
        (percent = (int)apr_atoi64(s)) < 0 || percent > 100) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get image conversion percentage");
        return NULL;
    }

    if ((s = apr_table_get(r->headers_in, CTR_IMG_COPY_HEADER)) == NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get a flag "
                      "whether image can attached in a mail");
        return NULL;
    }
    copy = (int)apr_atoi64(s);

    if ((s = apr_table_get(r->headers_in, CTR_IMG_MAINFRAME_HEADER)) == NULL ||
        (mframe = (int)apr_atoi64(s)) < 1) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get main frame number");
        return NULL;
    }

    if ((s = apr_table_get(r->headers_in, CTR_IMG_DSTFMT_HEADER)) == NULL ||
        (!(strcasecmp(s, "gif") == 0 && (fmt = PC_GIF)) &&
         !((strcasecmp(s, "jpg") == 0 || strcasecmp(s, "jpeg") == 0 ||
            strcasecmp(s, "jpz") == 0) && (fmt = PC_JPEG)) &&
         !((strcasecmp(s, "png") == 0 || strcasecmp(s, "pnz") == 0) &&
           (fmt = PC_PNG256)) &&
         !(strcasecmp(s, "bmp4") == 0 && (fmt = PC_BMP4)) &&
         !(strcasecmp(s, "bmp2") == 0 && (fmt = PC_BMP2)))) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get destination image format");
        return NULL;
    }
    suffix = s;

    /*
     * Unset all headers for image conversion to prevent double conversion.
     */
    apr_table_unset(r->headers_in, CTR_IMG_CONVMODE_HEADER);
    apr_table_unset(r->headers_in, CTR_IMG_PERCENT_HEADER);
    apr_table_unset(r->headers_in, CTR_IMG_COPY_HEADER);
    apr_table_unset(r->headers_in, CTR_IMG_DSTFMT_HEADER);
    apr_table_unset(r->headers_in, CTR_IMG_MAINFRAME_HEADER);

    switch (fmt) {
    case PC_GIF:
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: convert image to GIF");
        r->content_type = "image/gif";
        if (brw->carrier == CR_SOFTBANK && !copy)
            apr_table_setn(r->headers_out,
                           "x-jphone-copyright", "no-transfer");
        sfmt = "gif";
        depth = 8;
        colors = 256;
        quality = 100;
        break;
    case PC_PNG256:
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: convert image to PNG");
        r->content_type = "image/png";
        sfmt = "png";
        depth = 8;
        colors = 256;
        quality = 100;
        break;
    case PC_BMP4:
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: convert image to BMP4");
        r->content_type = "image/bmp";
        sfmt = "bmp";
        depth = 8;
        colors = 4;
        quality = 100;
        break;
    case PC_BMP2:
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: convert image to BMP2");
        r->content_type = "image/bmp";
        sfmt = "bmp";
        depth = 8;
        colors = 2;
        quality = 100;
        break;
    case PC_JPEG:
    default:
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: convert image to JPEG");
        r->content_type = "image/jpeg";
        sfmt = "jpeg";
        depth = 24;
        colors = brw->colors;
        quality = 91;
        break;
    }

    maxsize = ctr_imgmax_calc(max, brw, pool);
    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "mod_cthree: maxsize %ld", maxsize);

    if ((fdir = ctr_imgdir_get(cachedir, r)) == NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get image cache directory");
        return NULL;
    }

    rv = apr_dir_make_recursive(fdir, APR_OS_DEFAULT, pool);
    if (rv != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to create image cache directory");
        return NULL;
    }

    if ((fname = ctr_imgfname_get(fdir, brw, cvmode, (int)percent, copy,
                                  mframe, maxsize, suffix, pool)) == NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get image cache file path");
        return NULL;
    }

    /*
     * Check if image cache file is available.  If so, we don't need to convert
     * the image.
     */
    if (apr_file_open(&fd, fname, APR_READ | APR_BINARY,
                      APR_OS_DEFAULT, pool) == APR_SUCCESS) {
        /* image cache file is found */
        if ((rv = apr_file_lock(fd, APR_FLOCK_SHARED)) != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                          "mod_cthree: failed to lock image cache file");
            goto errout2;
        }

        b = ctr_bucket_create_from_imgfile(fd, last_modified, r);
        apr_file_close(fd);
        if (b != NULL) {
            /* image cache file is available */
            return b;
        }
    }

    if ((rv = apr_file_open(&fd, fname, APR_WRITE | APR_CREATE | APR_BINARY,
                            APR_OS_DEFAULT, pool)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to open image cache file");
        return NULL;
    }

    if ((rv = apr_file_lock(fd, APR_FLOCK_EXCLUSIVE)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to lock image cache file");
        goto errout2;
    }

    /*
     * Re-check if image cache file is available since it is possible that
     * another thread/process has created it while we are waiting on img lock.
     */
    if ((b = ctr_bucket_create_from_imgfile(fd, last_modified, r)) != NULL) {
        /* image cache file is available */
        apr_file_close(fd);
        return b;
    }

    if ((rv = apr_file_trunc(fd, (apr_off_t)0)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to truncate image cache file");
        goto errout2;
    }

    if (brw->colors == -1 ||
        (cvmode == CV_NORMAL &&
         (brw->pixwidth == -1 || brw->pixheight == -1)) ||
        (cvmode == CV_WALLPAPER &&
         (brw->wpixwidth == -1 || brw->wpixheight == -1)) ||
        brw->pictypes == NULL) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to get browser information "
                      "needed for image conversion");
        goto errout2;
    }

    wand = NewMagickWand();

    if (MagickReadImageBlob(wand, data, datalen) == MagickFalse) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to read image data from memory");
        goto errout1;
    }

    nimg = MagickGetNumberImages(wand);

#if 0
    if (MagickProfileImage(wand, "*", NULL, 0) == MagickFalse) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to remove image profiles");
        goto errout1;
    }
#else
    if (MagickStripImage(wand) == MagickFalse) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to strip image");
        goto errout1;
    }
#endif
#if MagickLibVersion >= 0x623
    MagickSetImageAttribute(wand, "JPEG-Colorspace", NULL);
    MagickSetImageAttribute(wand, "JPEG-Sampling-factors", NULL);
#endif

    if ((fmt == PC_GIF || fmt == PC_JPEG) &&
        (brw->carrier == CR_DOCOMO || brw->carrier == CR_KDDI)) {
        char *comment = NULL;

        if (brw->carrier == CR_DOCOMO && !copy)
            comment = "copy=\"NO\"";
        else if (!copy)
            comment = "kddi_copyright=on,";
        else
            comment = "kddi_copyright=off,";

        if (comment != NULL) {
            if (MagickCommentImage(wand, comment) == MagickFalse) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: failed to add comment to image");
                goto errout1;
            }
        }
    }

    if (nimg > 1) {
        MagickWand *wand2;

        if ((wand2 = MagickCoalesceImages(wand)) == NULL) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to coalesce image");
            goto errout1;
        }
        DestroyMagickWand(wand);
        wand = wand2;

        if (fmt != PC_GIF && mframe <= nimg) {
            MagickSetImageIndex(wand, mframe - 1);
            wand2 = MagickGetImage(wand);
            DestroyMagickWand(wand);
            wand = wand2;
        }
    }

    if (!ctr_img_scale(wand, cvmode, percent, fmt, brw)) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to resize image");
        goto errout1;
    }

    if (fmt == PC_GIF && nimg > 1) {
        if (ctr_imglayers_optimize(wand) == FALSE) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to optimize image layers");
            goto errout1;
        }
    }

#if MagickLibVersion >= 0x626
    if ((fmt == PC_PNG256 &&
         brw->carrier == CR_KDDI && brw->spec.kddi.iswap20 == FALSE) ||
        fmt == PC_JPEG) {
        PixelWand *fromc, *toc;

        fromc = NewPixelWand();
        toc = NewPixelWand();
        PixelSetColor(toc, "white");

        do {
            MagickGetImageBackgroundColor(wand, fromc);
            MagickPaintOpaqueImage(wand, fromc, toc, 0.0);
        } while (MagickNextImage(wand) == MagickTrue);
        MagickSetImageIndex(wand, 0);

        DestroyPixelWand(fromc);
        DestroyPixelWand(toc);

        MagickSetImageMatte(wand, MagickFalse);
    }
#endif

    if (MagickSetImageFormat(wand, sfmt) == MagickFalse) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to set image format");
        goto errout1;
    }

    if (MagickSetImageDepth(wand, depth) == MagickFalse) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to set image depth");
        goto errout1;
    }

    if ((rv = apr_temp_dir_get(&tdir, pool)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to get temporary directory");
        goto errout1;
    }

    if (apr_filepath_merge(&tfname, tdir,
                           "c3imgtemp_XXXXXX", 0, pool) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to get temporary image file path");
        goto errout1;
    }

    if ((rv = apr_file_mktemp(&tfd, tfname,
                              APR_READ | APR_WRITE | APR_CREATE |
                              APR_EXCL | APR_BINARY,
                              pool)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to create temporary image file");
        goto errout1;
    }
    apr_file_close(tfd);

    /*
     * Loop while the size of image file is bigger than the cache size of the
     * browser.
     */
    for (;;) {
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: colors %d", colors);
        ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                      "mod_cthree: quality %d", quality);

        if (MagickQuantizeImages(wand, colors, RGBColorspace, 0,
                                 colors <= 4 ? MagickTrue : MagickFalse,
                                 MagickTrue) == MagickFalse) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to quantize image");
            apr_file_remove(tfname, pool);
            goto errout1;
        }

        if (MagickSetImageCompressionQuality(wand, quality) == MagickFalse) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to set "
                          "image compression quality");
            goto errout1;
        }

        if (fmt == PC_GIF && nimg > 1) {
            if (MagickWriteImages(wand, tfname, MagickTrue) == MagickFalse) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: failed to write data "
                              "to temporary image file");
                apr_file_remove(tfname, pool);
                goto errout1;
            }
        } else {
            if (MagickWriteImage(wand, tfname) == MagickFalse) {
                ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                              "mod_cthree: failed to write data "
                              "to temporary image file");
                apr_file_remove(tfname, pool);
                goto errout1;
            }
        }

        rv = apr_stat(&tfinfo, tfname, APR_FINFO_SIZE, pool);
        if (rv != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                          "mod_cthree: failed to get stats "
                          "of temporary image file");
            apr_file_remove(tfname, pool);
            goto errout1;
        }

        if (maxsize == -1 || tfinfo.size <= maxsize) {
            /*
             * The size of image file is smaller than the cache size of the
             * browser.  Ok, now ready to output.
             */
            break;
        } else {
            /*
             * In JPEG case, reduce quality of the image.  Otherwise, reduce
             * colors.
             */
            if (fmt == PC_JPEG)
                quality = quality - 5;
            else
                colors = colors * 2 / 3;
        }

        if (colors <= 1 || quality < 1) {
            /*
             * I think it makes no sence to reduce quality/colors furthermore,
             * since the image is too bad to see.  So give up here...
             */
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: image is too big "
                          "to display on this browser");
            apr_file_remove(tfname, pool);
            goto errout1;
        }
    }

    DestroyMagickWand(wand);

    if (fmt == PC_PNG256 && brw->carrier == CR_KDDI && !copy) {
        /*
         * Add Copyright comment to PNG image not to attach the image in a
         * mail.  Because ImageMagick's MagickCommentImage() or
         * MagickSetImageAttribute() function is not good for PNG image, do it
         * manually.
         */
        char *tfname2;
        apr_file_t *tfd2;

        if (apr_filepath_merge(&tfname2, tdir,
                               "c3imgtemp_XXXXXX", 0, pool) != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to get "
                          "temporary image file path");
            apr_file_remove(tfname, pool);
            goto errout2;
        }

        if ((rv = apr_file_mktemp(&tfd2, tfname2,
                                  APR_READ | APR_WRITE | APR_CREATE |
                                  APR_EXCL | APR_BINARY,
                                  pool)) != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                          "mod_cthree: failed to create temporary image file");
            apr_file_remove(tfname, pool);
            goto errout1;
        }

        if (ctr_png_comment(tfname, tfd2, "Copyright", "on", pool) == FALSE) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to add comment to image");
            apr_file_remove(tfname, pool);
            apr_file_close(tfd2);
            apr_file_remove(tfname2, pool);
            goto errout2;
        }

        apr_file_close(tfd2);
        if ((rv = apr_file_rename(tfname2, tfname, pool)) != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                          "mod_cthree: failed to rename temporary image file");
            apr_file_remove(tfname, pool);
            apr_file_remove(tfname2, pool);
            goto errout2;
        }

        /*
         * Re-get file stat because it has been changed after adding a comment.
         */
        rv = apr_stat(&tfinfo, tfname, APR_FINFO_SIZE, pool);
        if (rv != APR_SUCCESS) {
            ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                          "mod_cthree: failed to get stats "
                          "of temporary image file");
            apr_file_remove(tfname, pool);
            goto errout2;
        }
    }

    if ((rv = apr_file_open(&tfd, tfname, APR_READ | APR_BINARY,
                            APR_OS_DEFAULT, pool)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, 0, r,
                      "mod_cthree: failed to open temporary image file");
        apr_file_remove(tfname, pool);
        goto errout2;
    }

    tfsize = tfinfo.size;
    buf = apr_palloc(pool, tfsize);
    rv = apr_file_read_full(tfd, buf, tfsize, NULL);
    if (rv != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to read temporary image file");
        apr_file_close(tfd);
        apr_file_remove(tfname, pool);
        goto errout2;
    }

    apr_file_close(tfd);
    apr_file_remove(tfname, pool);

    rv = apr_file_write_full(fd, buf, tfsize, NULL);
    if (rv != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to write to image cache file");
        goto errout2;
    }

    b = apr_bucket_pool_create(buf, tfsize, pool, conn->bucket_alloc);
    apr_file_mtime_set(fname, last_modified, pool);
    apr_file_close(fd);
    return b;

errout1:
    DestroyMagickWand(wand);
errout2:
    apr_file_close(fd);
    apr_file_remove(fname, pool);
    return NULL;
}

static uint32_t ctr_img_suffix_to_num(const char *suffix)
{
    int i;

    for (i = 0; i < sizeof(img_suffixes) / sizeof(char *); i++) {
        if (strcmp(suffix, img_suffixes[i]) == 0)
            return i;
    }

    return 0;
}

static const char *ctr_img_num_to_suffix(uint32_t n)
{
    if (n >=  sizeof(img_suffixes) / sizeof(char *))
        return "";

    return img_suffixes[n];
}

static long ctr_imgmax_calc(const char *smax, const ctr_browser_t *brw,
                            apr_pool_t *pool)
{
    unsigned idx;
    long maxsize;
    char *s;
    double d;

    if (smax != NULL) {
        /*
         * Maximum size of converted image is specified.  Use it.
         */
        idx = strlen(smax);
        idx--;
        if (smax[idx] == '%') {
            if (brw->cachesize == -1)
                maxsize = -1;
            else {
                s = apr_pstrndup(pool, smax, idx);
                d = (double)apr_atoi64(s);
                d *= 0.01;
                maxsize = brw->cachesize * d;
            }
        } else {
            maxsize = (long)apr_atoi64(smax);
            if (maxsize > brw->cachesize)
                maxsize = brw->cachesize;
        }
    } else {
        /*
         * Automatically calculate maximum size of converted image.
         */
        if (brw->cachesize == -1)
            maxsize = -1;
        else if (!brw->iscacheincboth)
            maxsize = brw->cachesize;
        else if (brw->cachesize >= 10 * 1024)
            maxsize = brw->cachesize - 4 * 1024;
        else
            maxsize = brw->cachesize / 2;
    }

    return maxsize;
}

static char *ctr_imgdir_get(const char *basedir, const request_rec *r)
{
    char *dir;

    dir = r->uri;
    if (*dir == '/')
        dir++;

    if (apr_filepath_merge(&dir, basedir, dir, 0, r->pool) != APR_SUCCESS)
        return NULL;
    else
        return dir;
}

static char *ctr_imgfname_get(const char *dir, const ctr_browser_t *brw,
                              ctr_convmode_t cvmode, int percent, int copy,
                              int mframe, long maxsize, const char *suffix,
                              apr_pool_t *pool)
{
    char *fname;

    fname = apr_psprintf(pool, "%d_%s_%d_%d_%d_%d_%ld.%s",
                         brw->carrier, brw->name ? brw->name : "",
                         cvmode, percent, copy, mframe, maxsize, suffix);
    if (apr_filepath_merge(&fname, dir, fname, 0, pool) != APR_SUCCESS)
        return NULL;
    else
        return fname;
}

static int ctr_img_scale(MagickWand *wand, ctr_convmode_t cvmode,
                         double percent, ctr_picformat_t fmt,
                         const ctr_browser_t *brw)
{
    int width, height;
    int imgwidth, imgheight;
    double scale;

    if (cvmode == CV_NORMAL) {
        width = brw->pixwidth;
        height = brw->pixheight;
    } else if (cvmode == CV_WALLPAPER) {
        width = brw->wpixwidth;
        height = brw->wpixheight;
    } else
        return FALSE;

    if (width == -1 || height == -1)
        return FALSE;

    MagickSetImageIndex(wand, 0);
    imgwidth = (int)MagickGetImageWidth(wand);
    imgheight = (int)MagickGetImageHeight(wand);
    scale = (width * (percent * 0.01)) / imgwidth;
    if (imgheight * scale > height)
        scale = (height * (percent * 0.01)) / imgheight;

    do {
        imgwidth = MagickGetImageWidth(wand);
        imgheight = MagickGetImageHeight(wand);
        imgwidth = imgwidth * scale;
        imgheight = imgheight * scale;
        if (fmt == PC_PNG256) {
            if (MagickSampleImage(wand, imgwidth, imgheight) == MagickFalse)
                return FALSE;
        } else {
            if (MagickScaleImage(wand, imgwidth, imgheight) == MagickFalse)
                return FALSE;
        }
    } while (MagickNextImage(wand) == MagickTrue);

    MagickSetImageIndex(wand, 0);
    return TRUE;
}

static int ctr_imglayers_optimize(MagickWand *wand)
{
#if MagickLibVersion >= 0x627
    int nimg;
    MagickWand *wand2;

    nimg = MagickGetNumberImages(wand);

    MagickSetImageIndex(wand, 0);
    wand2 = MagickGetImage(wand);

    MagickSetImageIndex(wand, nimg - 1);
    if (MagickAddImage(wand, wand2) == MagickFalse) {
        DestroyMagickWand(wand2);
        return FALSE;
    }

    DestroyMagickWand(wand2);

    MagickSetImageIndex(wand, 0);
    if (MagickOptimizeImageLayers(wand) == MagickFalse)
        return FALSE;

    MagickSetImageIndex(wand, nimg - 1);
    if (MagickRemoveImage(wand) == MagickFalse)
        return FALSE;

    MagickSetImageIndex(wand, 0);
#endif
    return TRUE;
}

static apr_bucket *ctr_bucket_create_from_imgfile(apr_file_t *fd,
                                                  apr_time_t last_modified,
                                                  const request_rec *r)
{
    apr_pool_t *pool = r->pool;
    conn_rec *conn = r->connection;
    apr_finfo_t finfo;
    apr_size_t fsize;
    char *s;
    apr_status_t rv;

    rv = apr_file_info_get(&finfo, APR_FINFO_SIZE | APR_FINFO_MTIME, fd);
    if (rv != APR_SUCCESS || finfo.size == 0 || finfo.mtime != last_modified)
        return NULL;

    ap_log_rerror(APLOG_MARK, APLOG_DEBUG, 0, r,
                  "mod_cthree: image cache file is available");

    fsize = finfo.size;
    s = apr_palloc(pool, fsize);
    if ((rv = apr_file_read_full(fd, s, fsize, NULL)) != APR_SUCCESS) {
        ap_log_rerror(APLOG_MARK, APLOG_ERR, rv, r,
                      "mod_cthree: failed to read image cache file");
        return NULL;
    }

    return apr_bucket_pool_create(s, fsize, pool, conn->bucket_alloc);
}

static int ctr_pictype_search(const ctr_pictype_t *pt, ctr_picformat_t fmt)
{
    for (; pt != NULL; pt = pt->next) {
        if (pt->type == fmt)
            return TRUE;
    }
    return FALSE;
}

static int ctr_png_comment(const char *in, apr_file_t *out, const char *key,
                           const char *val, apr_pool_t *pool)
{
    apr_file_t *infd;
    char *buf;
    uint32_t len;
    uint32_t c;
    int i;
    apr_size_t n;
    apr_status_t rv;

    if (apr_file_open(&infd, in, APR_READ | APR_BINARY,
                      APR_OS_DEFAULT, pool) != APR_SUCCESS)
        return FALSE;

    buf = apr_palloc(pool, 33); /* 33 is length of SIGNATURE + IHDR */
    if (apr_file_read_full(infd, buf, 33, NULL) != APR_SUCCESS)
        goto errout;
    if (apr_file_write_full(out, buf, 33, NULL) != APR_SUCCESS)
        goto errout;

    len = strlen(key) + 1 + strlen(val);        /* 1 is length of separator */
    buf[0] = len >> 24;
    buf[1] = len >> 16;
    buf[2] = len >> 8;
    buf[3] = len;
    if (apr_file_write_full(out, buf, 4, NULL) != APR_SUCCESS)
        goto errout;

    buf = apr_psprintf(pool, "%s%s%c%s", "tEXt", key, '\0', val);
    if (apr_file_write_full(out, buf, 4 + len, NULL) != APR_SUCCESS)
        goto errout;

    /*
     * Calculate CRC.
     */
    c = 0xffffffff;
    for (i = 0; i < 4 + len; i++)
        c = crc[(c ^ buf[i]) & 0xff] ^ (c >> 8);
    c ^= 0xffffffff;

    buf[0] = c >> 24;
    buf[1] = c >> 16;
    buf[2] = c >> 8;
    buf[3] = c;
    if (apr_file_write_full(out, buf, 4, NULL) != APR_SUCCESS)
        goto errout;

    buf = apr_palloc(pool, 1024);       /* XXX: 1024 */
    n = 1024;
    while ((rv = apr_file_read(infd, buf, &n)) == APR_SUCCESS) {
        if (apr_file_write(out, buf, &n) != APR_SUCCESS)
            goto errout;
        n = 1024;
    }
    if (rv != APR_EOF)
        goto errout;

    apr_file_close(infd);
    return TRUE;

errout:
    apr_file_close(infd);
    return FALSE;
}
