Image Scaling in C

hypnotoad writes: While pure-script image scaling is "nice" for one-off projects, many of us need something that is fast, and capable of scaling images into arbitrary scale factors. Below is a routine that is based in C that we use in production to scale ship templates to fit on a canvas for tracing. As users need different levels of detail, it had to scale and line up pixel-for pixel for what was being overlaid on the canvas.

/*
 * save as "imgscale.c", compile with:
 * $ gcc -shared -o imgscale.so -I/usr/include/tcl8.5 imgscale.c -DUSE_TCL_STUBS -DUSE_TK_STUBS -ltclstub8.5 -ltkstub8.5
 */
/*
** Scale an image using grid sampling
** Please note this implementation will crash if the
** destination image was not already created 
**
** Arguments: srcimg newwidth newheight destimg alpha
** srcimg: A photo object created with "image create photo"
** newwidth: The new width of the image in pixels
** newheight: The new height of the image in pixels
** destimage: Name for the resulting image
** alpha: Alpha to apply to the image (1 - opaque, 0-clear)
*/

#include <tcl.h>
#include <tk.h> 

static int irmScaleImgCmd(
  void *pArg,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *CONST objv[]
){
    char *srcName, *destName;
    Tk_PhotoImageBlock srcBlock, destBlock;
    Tk_PhotoHandle srcImage, destImage;
    int di, dj;
    double scalex, scaley, sx2, sy2, newalpha;
    int returnCode;
    int width, height, newwid, newhgt;

    if (objc != 5 && objc != 6) {
        Tcl_WrongNumArgs(interp, 1, objv, "srcimg newwid newhgt destimg ?alpha?");
        return TCL_ERROR;
    }
    srcName = Tcl_GetString(objv[1]);
    if (Tcl_GetIntFromObj(interp, objv[2], &newwid))
[]

        return TCL_ERROR;
    if (Tcl_GetIntFromObj(interp, objv[3], &newhgt))
        return TCL_ERROR;
    destName=Tcl_GetString(objv[4]);
    if (objc == 6) {
        if (Tcl_GetDoubleFromObj(interp, objv[5], &newalpha))
            return TCL_ERROR;
    } else {
        newalpha=1.0;
    }
    if (newalpha>1.0) {
        newalpha=1.0;
    }

    srcImage = Tk_FindPhoto(interp, srcName);
    if (!srcImage)
        return TCL_ERROR;
    Tk_PhotoGetSize(srcImage, &width, &height);
    Tk_PhotoGetImage(srcImage, &srcBlock);

    if (!srcBlock.pixelPtr) {
        /* Empty image. Do nothing */
        return TCL_OK;
    }

    if (srcBlock.pixelSize != 4 && srcBlock.pixelSize!=3) {
        Tcl_AppendResult(interp, "I can't make heads or tails from this image, the bitfield is neither 3 nor 4", NULL);
        return TCL_ERROR;
    }

    destImage = Tk_FindPhoto(interp, destName);
    if (!destImage)
        return TCL_ERROR;
    Tk_PhotoBlank(destImage);
    Tk_PhotoSetSize(interp, destImage, newwid, newhgt);    

    destBlock.width = newwid;
    destBlock.height = newhgt;
    
    scalex = srcBlock.width / (double) newwid;
    scaley = srcBlock.height / (double) newhgt;
    sx2 = scalex / 2.0;
    sy2 = scaley / 2.0;

    destBlock.pixelSize = 4;
    destBlock.pitch = newwid * 4;
    destBlock.offset[0] = 0; 
    destBlock.offset[1] = 1; 
    destBlock.offset[2] = 2; 
    destBlock.offset[3] = 3;
    destBlock.pixelPtr = (unsigned char *) Tcl_Alloc(destBlock.width * destBlock.height * 4);

    /* Loop through and scale */
    for (dj=0 ; dj<destBlock.height ; dj++) {
        for (di=0 ; di<destBlock.width ; di++) {
            int si, sj;
            int cx = (int)(di * scalex);
            int cy = (int)(dj * scaley);
            int points = 1;
            double red = 0.0, green = 0.0, blue = 0.0, alpha = 0.0;
            int newoff = destBlock.pitch*dj + destBlock.pixelSize*di;
            int startoff = srcBlock.pitch*cy + srcBlock.pixelSize*cx;

            red = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[0]];
            green = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[1]];
            blue = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[2]];
            if (srcBlock.pixelSize == 4) {
                alpha = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[3]];
            } else {
                alpha += 255;
            }
            for (sj=(int)cy-sy2 ; sj<(int)cy+sy2 ; sj++) {
                if (sj < 0)
                    continue;
                if (sj > srcBlock.height)
                    continue;
                for (si=(int)cx-sx2 ; si<(int)cx+sx2 ; si++) {
                    int offset = srcBlock.pitch*sj + srcBlock.pixelSize*si;
                    if (si < 0)
                        continue;
                    if (si > srcBlock.width)
                        continue;

                    points++;
                    red += (double) srcBlock.pixelPtr[offset + srcBlock.offset[0]];
                    green += (double) srcBlock.pixelPtr[offset + srcBlock.offset[1]];
                    blue += (double) srcBlock.pixelPtr[offset + srcBlock.offset[2]];
                    if (srcBlock.pixelSize == 4) {
                        alpha += (double) srcBlock.pixelPtr[offset + srcBlock.offset[3]];
                    } else {
                        alpha += 255;
                    }
                }
            }
            destBlock.pixelPtr[newoff + destBlock.offset[0]] = (unsigned char)(red / points);
            destBlock.pixelPtr[newoff + destBlock.offset[1]] = (unsigned char)(green / points);
            destBlock.pixelPtr[newoff + destBlock.offset[2]] = (unsigned char)(blue / points);
            destBlock.pixelPtr[newoff + destBlock.offset[3]] = (unsigned char)(alpha*newalpha / points);
        }
    }
    returnCode = Tk_PhotoPutBlock(interp, destImage, &destBlock, 0, 0,
            destBlock.width, destBlock.height, TK_PHOTO_COMPOSITE_SET);
    return returnCode;
}


/*
** The following is the only public symbol in this source file.  .
*/
int DLLEXPORT Imgscale_Init(Tcl_Interp *interp){
    if(Tcl_InitStubs(interp, TCL_VERSION, 0) == NULL) {
        return TCL_ERROR;
    }
    if(Tk_InitStubs(interp, TK_VERSION, 0) == NULL) {
        return TCL_ERROR;
    }
    Tcl_CreateObjCommand(interp, "image_scale", irmScaleImgCmd, 0, 0);
    return TCL_OK;
}

2014-06-06

arw: I wrapped the above code in an Imagescale TEA package and placed it on github.com . This might help some of you to compile it under Windows, OSX and Linux.


RZ - 2010-02-04 10:34:03

Any chance of including this in the tk [image] command?


aspect: stubsified and added build instructions with help from the irc channel.


EG: Added a check for an empty source image, which leads to a segmentation fault.


ralfixx Note: it seems TK 8.5 is required for this. I get SEGVs with 8.3.3


hypnotoad The problem is pre 8.5 Tk core's don't do Alpha blending. While I was experimenting, I did come up with an alternate algorithm that de-saturated the image. Here's that version:

/*
** Scale an image using grid sampling
** Please note this implementation will crash if the
** destination image was not already created 
*/

#include <tcl.h>
#include <tk.h> 

static int irmScaleImgNoAlphaCmd(
  void *pArg,
  Tcl_Interp *interp,
  int objc,
  Tcl_Obj *CONST objv[]
){
    char *srcName, *destName;
    Tk_PhotoImageBlock srcBlock, destBlock;
    Tk_PhotoHandle srcImage, destImage;
    int di,dj;
    double scalex, scaley, sx2, sy2, newalpha;
    int returnCode;
    int width, height, newwid, newhgt;

    if (objc != 5 && objc !=6){
        Tcl_WrongNumArgs(interp, 1, objv, "srcimg newwid newhgt destimg ?alpha?");
        return TCL_ERROR;
    }
    srcName=Tcl_GetString(objv[1]);
    if (Tcl_GetIntFromObj(interp,objv[2], &newwid))
        return TCL_ERROR;
    if (Tcl_GetIntFromObj(interp,objv[3], &newhgt))
        return TCL_ERROR;
    destName = Tcl_GetString(objv[4]);
    if (objc==6) {
        if (Tcl_GetDoubleFromObj(interp,objv[5], &newalpha))
            return TCL_ERROR;
    } else {
        newalpha = 1.0;
    }
    if (newalpha > 1.0) {
        newalpha = 1.0;
    }

    srcImage = Tk_FindPhoto(interp, srcName);
    if (!srcImage)
        return TCL_ERROR;
    Tk_PhotoGetSize(srcImage, &width, &height);
    Tk_PhotoGetImage(srcImage, &srcBlock);
    if (srcBlock.pixelSize != 4 && srcBlock.pixelSize!=3) {
        Tcl_AppendResult(interp, "I can't make heads or tails from this image, the bitfield is neither 3 nor 4", NULL);
        return TCL_ERROR;
    }

    destImage = Tk_FindPhoto(interp,  destName);
    if (!destImage)
        return TCL_ERROR;
    Tk_PhotoBlank(destImage);
    Tk_PhotoSetSize(interp, destImage, newwid, newhgt);    

    destBlock.width = newwid;
    destBlock.height = newhgt;
    
    scalex = srcBlock.width / (double)newwid;
    scaley = srcBlock.height / (double)newhgt;
    sx2 = scalex / 2.0;
    sy2 = scaley / 2.0;

    destBlock.pixelSize = 4;
    destBlock.pitch = newwid * 4;
    destBlock.offset[0] = 0; 
    destBlock.offset[1] = 1; 
    destBlock.offset[2] = 2; 
    destBlock.offset[3] = 3;
    destBlock.pixelPtr = (unsigned char *) Tcl_Alloc(destBlock.width * destBlock.height * 4);

    /* Loop through and scale */
    for (dj=0 ; dj<destBlock.height ; dj++) {
        for (di=0 ; di<destBlock.width ; di++) {
            int si, sj;
            int cx = (int)(di*scalex);
            int cy = (int)(dj*scaley);
            int points = 1;
            double red=0.0, green=0.0, blue=0.0, alpha=0.0;
            int newoff = destBlock.pitch*dj+destBlock.pixelSize*di;
            int startoff = srcBlock.pitch*cy+srcBlock.pixelSize*cx;

            red = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[0]];
            green = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[1]];
            blue = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[2]];
            if (srcBlock.pixelSize == 4) {
                alpha = (double) srcBlock.pixelPtr[startoff + srcBlock.offset[3]];
            } else {
                alpha = 255;
            }
            for (sj=(int)cy-sy2 ; sj<(int)cy+sy2 ; sj++) {
                if (sj < 0)
                    continue;
                if (sj > srcBlock.height)
                    continue;
                for (si=(int)cx-sx2 ; si<(int)cx+sx2 ; si++) {
                    int offset = srcBlock.pitch*sj + srcBlock.pixelSize*si;
                    if (si < 0)
                        continue;
                    if (si > srcBlock.width)
                        continue;
                    points++;
                    red += (double) srcBlock.pixelPtr[offset + srcBlock.offset[0]];
                    green += (double) srcBlock.pixelPtr[offset + srcBlock.offset[1]];
                    blue += (double) srcBlock.pixelPtr[offset + srcBlock.offset[2]];
                    if (srcBlock.pixelSize == 4) {
                        alpha += (double) srcBlock.pixelPtr[offset + srcBlock.offset[3]];
                    } else {
                        alpha += 255;
                    }
                }
            }
            /*
            ** As an experiment, instead of alpha blending we will desaturate
            ** the image
            */
            {
                int padding = 1.0-newalpha*255;
                double range = (255.0-padding)/255.0;
                destBlock.pixelPtr[newoff+destBlock.offset[0]] = (unsigned char)(red/points*range+padding);
                destBlock.pixelPtr[newoff+destBlock.offset[1]] = (unsigned char)(green/points*range+padding);
                destBlock.pixelPtr[newoff+destBlock.offset[2]] = (unsigned char)(blue/points*range+padding);
                /*destBlock.pixelPtr[newoff+destBlock.offset[3]]=255;*/
            }
        }
    }
    returnCode = Tk_PhotoPutBlock(interp, destImage, &destBlock, 0, 0,
            destBlock.width, destBlock.height, TK_PHOTO_COMPOSITE_SET);
    return returnCode;
}

RLE (2012-11-09): imgscale.c did not compile for me until I added

#include <tcl.h>
#include <tk.h>

above the function definition. I made the changes in the code above.