/**
 * \file logo.c
 * Composite an alpha-channel logo with optional drop-shadow onto video.
 *
 * Sponsored by Fabrik Inc. - bfree(at)fabrikinc.com
 * \author Bob (grafman) Free - bfree(at)graphcomp.com
 *
 * This vhook demonstrates the use of av_read_image to load still
 * images with alpha/transparency and composite them on video.
 * 
 * Note: PNG support requires that FFMPEG be built with the PNG
 * codec registered/enabled.
 *
 ******************************************************************************
 * EXAMPLE USAGE:
 *
 *   ffmpeg -i INFILE -vhook 'PATH/logo.so -f logo.gif' OUTFILE
 *
 *
 * Note: the entire vhook argument must be single-quoted.
 *
 *
 * REQUIRED ARGS:
 *
 * -f <FILEPATH>
 *
 *   Specifies the image to use for the logo.  GIF is supported
 *   in normal FFMPEG builds; PNG is supported if enabled in libavcodec
 *   and libavformat.
 *
 * 
 * OPTIONAL ARGS:
 *
 * -x <INT>
 *
 *   Defines a logo offset from the left side of the frame.
 *   A negative value (including -0) offsets from the right side.
 *
 * -y <INT>
 *
 *   Defines a logo offset from the top of the frame.
 *   A negative value (including -0) offsets from the bottom.
 *
 * -w <INT>
 *
 *   Defines a drop shadow to the right of the logo.
 *   A negative value shifts the shadow to the left.
 *
 * -h <INT>
 *
 *   Defines a drop shadow to the bottom of the logo.
 *   A negative value shifts the shadow upward.
 *
 * -d <INT>
 *
 *   Defines the percent opacity of the drop shadow (0 - 100);
 *   100 is opaque.  Defaults to 75.
 *
 *
 * Sample logos and additional notes available at
 * http://graphcomp.com/ffmpeg#plugins
 *
 ******************************************************************************
 * LICENSE:
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
 *
 */



/* Includes */

#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdarg.h>
#include <string.h>

#include "common.h"
#include "avformat.h"
#include "avcodec.h"
#include "allformats.h"

#include "framehook.h"
#include "cmdutils.h"



/* This stuff belongs in a header file */

/**
 * RGBA pixel.
 */
typedef struct
{
    uint8_t R; ///< Red.
    uint8_t G; ///< Green.
    uint8_t B; ///< Blue.
    uint8_t A; ///< Alpha.
} RGBA;

/**
 * RECT - bounding rectangle.
 */
typedef struct
{
    int left;
    int top;
    int right;
    int bottom;
} RECT;

/*
 * Constants.
 */
#define MAX_FILEPATH 2048             ///< Max filepath length.

/**
 * ContextInfo - shares data between vhook functions.
 */
typedef struct
{
    // User parameters - populated by Configure.
    char     filename[MAX_FILEPATH];  ///< Logo filepath.
    int      xDir;                    ///< Horizontal offset direction.
    int      yDir;                    ///< Vertical offset direction.
    int      xOff;                    ///< Horizontal offset in pixels.
    int      yOff;                    ///< Vertical offset in pixels.
    int      xDrop;                   ///< Horizontal drop-shadow offset.
    int      yDrop;                   ///< Vertical drop-shadow offset.
    int      dOpacity;                ///< Drop-shadow opacity (0-100).

    // Image cache buffers - populated by load_image.
    int      image_loaded;            ///< Image cache loaded flag.
    int      width;                   ///< Logo width in pixels.
    int      height;                  ///< Logo height in pixels.
    int      inp_fmt;                 ///< Logo format constant.
    int      inp_size;                ///< Logo buffer size in bytes.
    uint8_t* inp_buf;                 ///< Pointer to logo buffer.
    int      rgba_size;               ///< RGBA buffer size in bytes.
    uint8_t* rgba_buf;                ///< Pointer to RGBA buffer.
    AVFrame* rgbaFrame;               ///< Pointer to RGBA frame.

    // Bounds data - populated by calc_bounds.
    int      vidWidth;                ///< Video frame width in pixels.
    int      vidHeight;               ///< Video frame height in pixels.
    int	     vid_row;                 ///< Video row size in bytes.
    int	     logo_row;                ///< Logo row size in bytes.
    RECT     rLogo;                   ///< Logo placement on video frame.
    RECT     rDrop;                   ///< Drop-shadow placement on video frame.
    RECT     rBounds;                 ///< Affected pixels on video frame.
} ContextInfo;

/*
 * Macros.
 */
#define min(a,b) ((a < b) ? a : b)    ///< Return the smaller of 2 values.
#define max(a,b) ((a > b) ? a : b)    ///< Return the larger of 2 values.

/**
 * Allocate context block and capture user parameters.
 * Called by FFMPEG pipeline.
 * @param ctxp A handle to receive alloacted context pointer.
 * @param argc vhook's argument count.
 * @param argv vhook's argument pointers.
 * @return 0 for success; otherwise failure.
 */
int Configure(void **ctxp, int argc, char *argv[]);

/**
 * Release context block.
 * Called by FFMPEG pipeline.
 * @param ctx Context pointer.
 */
void Release(void *ctx);

/**
 * Main video frame proc.
 * Called by FFMPEG pipeline.
 * @param ctx Context pointer.
 * @param picture Pointer to video frame.
 * @param pix_fmt Video frame format constant.
 * @param src_width Video frame width in pixels.
 * @param src_heigth Video frame height in pixels.
 * @param pts Presentation timestamp.
 */
void Process
(
  void *ctx,
  AVPicture *picture,
  enum PixelFormat pix_fmt,
  int src_width,
  int src_height,
  int64_t pts
);

/**
 * Load and cache an image.
 * @param ci Context pointer.
 * @return 0 for success; otherwise failure.
 */
int load_image(ContextInfo *ci);

/**
 * Release an image cache.
 * @param ci Context pointer.
 */
void release_image(ContextInfo *ci);

/**
 * Initialization callback for av_read_image.
 * @param opaque Context pointer.
 * @param info Image info from codec.
 * @return 0 for success; otherwise failure.
 */
static int read_image_alloc_cb(void *opaque, AVImageInfo *info);

/**
 * Calculates bounding info for video frame, logo and drop-shadow.
 * @param ci Context pointer.
 * @param vid_width Video frame width in pixels.
 * @param vid_height Video frame height in pixels.
 */
void calc_bounds(ContextInfo *ci, int vid_width, int vid_height);

/**
 * Merges a pixel component onto another using and alpha-channel value.
 * @param back Background pixel value.
 * @param fore Foreground pixel value.
 * @param alpha Alpha-channel value - from 0.0 (transparent) to 1.0 (opaque).
 * @return Merged component value.
 */
int alpha_merge(int back, int fore, double alpha);

/*
 * Case-insensitive string compare.
 */
int stricmp (const char *s, const char *t);



/* Finally - the code */

/******************************************************************************
 * Allocate Context block and capture user parameters.
 ******************************************************************************/
int Configure(void **ctxp, int argc, char *argv[])
{
    ContextInfo *ci;
    int c;

    // Allocate context block
    if (0 == (*ctxp = av_mallocz(sizeof(ContextInfo)))) return -1;
    ci = (ContextInfo *)*ctxp;

    // Set drop shadow default to 75%
    ci->dOpacity = 75;

    // Parse user parameters
    opterr = 0;
    optind = 1;
    while ((c = getopt(argc, argv, "f:x::y::w::h::d::")) > 0)
    {
        switch (c)
        {
            // Logo filepath
            case 'f':
            {
                strncpy(ci->filename, optarg, MAX_FILEPATH-1);
                ci->filename[MAX_FILEPATH-1] = 0;
                break;
            }
            // Logo offset
            case 'x':
            {
                ci->xDir = strchr(argv[optind],'-') ? -1 : 1;
                ci->xOff = abs(atoi(argv[optind]));
                break;
            }
            case 'y':
            {
                ci->yDir = strchr(argv[optind],'-') ? -1 : 1;
                ci->yOff = abs(atoi(argv[optind]));
                break;
            }
            // Drop shadow offset
            case 'w':
            {
                ci->xDrop = atoi(argv[optind]);
                break;
            }
            case 'h':
            {
                ci->yDrop = atoi(argv[optind]);
                break;
            }
            // Drop shadow opacity
            case 'd':
            {
                ci->dOpacity = atoi(argv[optind]);
                if (ci->dOpacity < 0) ci->dOpacity = 0;
                if (ci->dOpacity > 100) ci->dOpacity = 100;
                break;
            }
            // Ignore unsupported args
            default:
            {
                av_log(NULL, AV_LOG_DEBUG,
                    "logo: Unrecognized argument '-%c %s' - ignored\n",
                    c,argv[optind]);
            }
        }
    }

    // Check that a filepath was provided
    if (0 == ci->filename[0])
    {
        av_log(NULL, AV_LOG_ERROR, "logo: No filepath specified.\n");
        return -1;
    }

    // Register codecs
    av_register_all();

    // Load and cache logo
    return(load_image(ci));
}


/****************************************************************************
 * Free Context block.
 ****************************************************************************/
void Release(void *ctx)
{
    ContextInfo *ci = (ContextInfo *)ctx;

    if (ci) release_image(ci);
    if (ctx) av_free(ctx);
}


/****************************************************************************
 * Main filter proc.
 ****************************************************************************/
void Process(void *ctx,
    AVPicture *picture,
    enum PixelFormat pix_fmt,
    int src_width,
    int src_height,
    int64_t pts)
{
    ContextInfo *ci;
    uint8_t *buf = 0;
    AVPicture *pict = picture;
    AVPicture rgbaPict;
    int x, y, xLogo, yLogo, xDrop, yDrop;
    int vid_offs, logo_offs, drop_offs;
    RGBA *pVid;
    RGBA *pLogo;
    RGBA *pDrop;
    double alpha;
    double opacity;

    // Skip if no frame dimensions
    if (!src_width || !src_height) return;

    // Initialize context
    ci = (ContextInfo *) ctx;
    calc_bounds(ci, src_width, src_height);

    // Convert video frame to RGBA32 (easier to process for palette-based videos)
    // Could optimize for non-palette based videos
    if (pix_fmt != PIX_FMT_RGBA32)
    {
        int size = avpicture_get_size(PIX_FMT_RGBA32, src_width, src_height);
        buf = av_malloc(size);

        avpicture_fill(&rgbaPict, buf, PIX_FMT_RGBA32, src_width, src_height);
        if (img_convert(&rgbaPict, PIX_FMT_RGBA32,
            picture, pix_fmt, src_width, src_height) < 0)
        {
            av_free(buf);
            return;
        }
        pict = &rgbaPict;
    }

    /* Insert filter code here, if any */

    // Frame's row loop
    for (y=ci->rBounds.top; y<ci->rBounds.bottom; y++)
    {
        // Get video frame pointer offset
        vid_offs = y * ci->vid_row;

        // Get logo pointer offset
        yLogo = y - ci->rLogo.top;
        logo_offs = yLogo * ci->logo_row;

        // Get drop-shadow pointer offset
        yDrop = y - ci->rDrop.top;
        drop_offs = yDrop * ci->logo_row;

        // Row's pixel loop
        for (x=ci->rBounds.left; x<ci->rBounds.right; x++)
        {
            // Get pointer to video frame pixel
            pVid = (RGBA *)(pict->data[0]+vid_offs+(x<<2));

            // Handle drop-shadow first - skip if no drop-shadow offsets
            if (ci->xDrop || ci->yDrop)
            {
                xDrop = x - ci->rDrop.left;

                if (xDrop > 0 && xDrop < ci->width &&
                    yDrop > 0 && yDrop < ci->height)
                {
                    // Get pointer to drop-shadow pixel
                    pDrop = (RGBA *)(ci->rgbaFrame->data[0]+drop_offs+(xDrop<<2));

                    // Lame drop shadow - gaussian distribution would be much better
                    opacity = ci->dOpacity * pDrop->A / 25500.0;

                    // Composite drop-shadow
                    if (opacity != 0.0)
                    {
                        pVid->R = alpha_merge(pVid->R,0,opacity);
                        pVid->G = alpha_merge(pVid->G,0,opacity);
                        pVid->B = alpha_merge(pVid->B,0,opacity);
                    }
                }
            }

            // Handle logo next
            xLogo = x - ci->rLogo.left;
            if (yLogo > 0 && yLogo < ci->height &&
                xLogo > 0 && xLogo < ci->width)
            {
                // Get pointer to logo pixel
                pLogo = (RGBA *)(ci->rgbaFrame->data[0]+logo_offs+(xLogo<<2));

                // If opaque, just copy
                if (pLogo->A == 255)
                {
                    *pVid = *pLogo;
                }
                // Skip if transparent - otherwise merge
                else if (pLogo->A)
                {
                    alpha = pLogo->A / 255.0;
                    pVid->R = alpha_merge(pVid->R,pLogo->R,alpha);
                    pVid->G = alpha_merge(pVid->G,pLogo->G,alpha);
                    pVid->B = alpha_merge(pVid->B,pLogo->B,alpha);
                }
            }
        } // foreach X
    } // foreach Y

    // Convert modified frame back to video format
    if (pix_fmt != PIX_FMT_RGBA32)
    {
        if (img_convert(picture, pix_fmt,
            &rgbaPict, PIX_FMT_RGBA32, src_width, src_height) < 0)
        {
            // Error handling here
        }
        av_free(buf);
        buf = 0;
    }
}


/****************************************************************************
 * Load and cache image buffers.
 ****************************************************************************/
int load_image(ContextInfo *ci)
{
    AVImageFormat *pFormat;
    ByteIOContext bctx,*pb=&bctx;
    AVFrame *pFrameInp;
    int err;

    // Just return if logo has already been fetched/converted
    if (ci->image_loaded) return 0;

    // Guess image format
    pFormat = guess_image_format(ci->filename);

    // JPEG image decoder is broken - bail
#if !defined(JPEG_FIXED)
    if (!strcmp(pFormat->name,"jpeg"))
    {
        av_log(NULL, AV_LOG_ERROR,"logo: JPEG image format not supported\n");
        return -1;
    }
#endif

    // Unable to guess format
    if (!pFormat)
    {
        av_log(NULL, AV_LOG_ERROR,"logo: Unsupported image format\n");
        return -1;
    }

    // Open file
    if (url_fopen(pb, ci->filename, URL_RDONLY) < 0)
    {
        av_log(NULL, AV_LOG_ERROR,"logo: Unable to open image\n");
        return -1;
    }

    // Read file
    err = av_read_image(pb, ci->filename, pFormat, read_image_alloc_cb, ci);
    url_fclose(pb);

    // Handle errors
    if (!ci->inp_buf)
    {
        av_log(NULL, AV_LOG_ERROR,"logo: Unable to allocate read buffer\n");
        return -1;
    }
    if (!ci->rgba_buf || !ci->rgbaFrame)
    {
        av_log(NULL, AV_LOG_ERROR,"logo: Unable to allocate rgba buffer\n");
        return -1;
    }
    if (err)
    {
        av_log(NULL, AV_LOG_ERROR,"logo: av_read_image error: %d\n",err);
        return -1;
    }

    // Convert to RGBA32 if necessary
    if (ci->inp_fmt != PIX_FMT_RGBA32)
    {
        // Allocate input frame
        pFrameInp = avcodec_alloc_frame();
        avpicture_fill((AVPicture*)pFrameInp,
            ci->inp_buf,ci->inp_fmt,ci->width,ci->height);

        img_convert((AVPicture*)ci->rgbaFrame, PIX_FMT_RGBA32,
            (AVPicture*)pFrameInp, ci->inp_fmt, ci->width, ci->height);

        av_free(pFrameInp);
    }

    // Done
    ci->image_loaded = 1;
    return(0);
}


/****************************************************************************
 * Release image buffers.
 ****************************************************************************/
void release_image(ContextInfo *ci)
{
    // Free RGBA format buffer
    if (ci->rgbaFrame) av_free(ci->rgbaFrame);
    ci->rgbaFrame = 0;
    if (ci->inp_fmt != PIX_FMT_RGBA32)
    {
        if (ci->rgba_buf) av_free(ci->rgba_buf);
        ci->rgba_buf = 0;
    }

    // Free input buffer
    if (ci->inp_buf) av_free(ci->inp_buf);
    ci->inp_buf = 0;

    ci->image_loaded = 0;
}


/****************************************************************************
 * Alloc callback for av_read_image.
 ****************************************************************************/
static int read_image_alloc_cb(void *opaque, AVImageInfo *info)
{
    ContextInfo *ci = opaque;

    // Capture image dimensions and pixel format
    if (!info->width || !info->height) return -1;
    ci->width = info->width;
    ci->height = info->height;
    ci->inp_fmt = info->pix_fmt;

    // Allocate input image buffer
    ci->inp_size = avpicture_get_size(info->pix_fmt,info->width,info->height);
    ci->inp_buf = av_malloc(ci->inp_size);

    // Map input frame to buffer
    avpicture_fill(&info->pict,ci->inp_buf,info->pix_fmt,info->width,info->height);

    // Input format is already PIX_FMT_RGBA32
    if (ci->inp_fmt == PIX_FMT_RGBA32)
    {
        ci->rgba_size = ci->inp_size;
        ci->rgba_buf = ci->inp_buf;
    }
    // Otherwise allocate rgba buffer
    else
    {
        ci->rgba_size = avpicture_get_size(PIX_FMT_RGBA32,info->width,info->height);
        ci->rgba_buf = av_malloc(ci->rgba_size);
    }

    // Map RGBA frame to buffer
    ci->rgbaFrame = avcodec_alloc_frame();
    avpicture_fill((AVPicture*)ci->rgbaFrame,
        ci->rgba_buf,PIX_FMT_RGBA32,info->width,info->height);

    return(0);
}


/****************************************************************************
 * Calculate and cache bounds.
 ****************************************************************************/
void calc_bounds(ContextInfo *ci, int vid_width, int vid_height)
{
    // skip if already cached
    if (ci->vidWidth && ci->vidHeight) return;

    // Cache video frame dimensions
    ci->vidWidth = vid_width;
    ci->vidHeight = vid_height;

    // Calculate row sizes - assume 4 bytes/pixel for RGBA
    ci->vid_row = vid_width << 2;
    ci->logo_row = ci->width << 2;

    // Calculate logo position on frame
    if (ci->xDir < 0)
    {
        ci->rLogo.right = ci->vidWidth - ci->xOff;
        ci->rLogo.left = ci->rLogo.right - ci->width;
    }
    else
    {
        ci->rLogo.left = ci->xOff;
        ci->rLogo.right = ci->rLogo.left + ci->width;
    }
    if (ci->yDir < 0)
    {
        ci->rLogo.bottom = ci->vidHeight - ci->yOff;
        ci->rLogo.top = ci->rLogo.bottom - ci->height;
    }
    else
    {
        ci->rLogo.top = ci->yOff;
        ci->rLogo.bottom = ci->rLogo.top + ci->height;
    }

    // Calculate drop shadow position on frame
    ci->rDrop.left = ci->rLogo.left + ci->xDrop;
    ci->rDrop.right = ci->rLogo.right + ci->xDrop;
    ci->rDrop.top = ci->rLogo.top + ci->yDrop;
    ci->rDrop.bottom = ci->rLogo.bottom + ci->yDrop;

    // Calculate combined logo/drop-shadow bounds
    ci->rBounds.left = max(0,min(vid_width,min(ci->rLogo.left,ci->rDrop.left)));
    ci->rBounds.top = max(0,min(vid_height,min(ci->rLogo.top,ci->rDrop.top)));
    ci->rBounds.right = min(vid_width,max(0,max(ci->rLogo.right,ci->rDrop.right)));
    ci->rBounds.bottom = min(vid_height,max(0,max(ci->rLogo.bottom,ci->rDrop.bottom)));
}


/****************************************************************************
 * Very simple alpha merge.
 *
 * alpha is 0.0 (transparent) to 1.0 (opaque).
 ****************************************************************************/
int alpha_merge(int back, int fore, double alpha)
{
    int val = (back * (1.0 - alpha)) + (fore * alpha);
    return(min(255,(max(0,val))));
}


/****************************************************************************
 * Is there a portable version of this?
 *
 * 0 = string found; otherwise not
 ****************************************************************************/
int stricmp (const char *s, const char *t)
{
    int d = 0;
    do
    {
        d = toupper(*s) - toupper(*t);
    } while (*s++ && *t++ && !d);
    return(d);
}

