4

I have a simple REST API built in ColdFusion 11 that allows our vendor to update product information. Most of the data they send (about 30 attributes) is simple values - strings and numbers. One attribute is a Base64 encoded string that represents an image file. The REST API updates the simple data values in the database, and then reads the image and resizes it into 4 size that are suitable for use on the website. I've noticed that occasionally the vendor will send large images with some requests (20MB+) and that these requests often result in a "java.lang.OutOfMemoryError: Java heap space" on the line where the ImageReadBase64() function is called. We have other web apps running on this server and it seems that the memory that gets locked up processing these images causes problem with those apps as well. I'm trying to figure out if there's anything that I can do in the code to reduce these issues, even if it's figuring out a way to NOT process the images if they are "too big" (over a certain size). Here is all the code that is run to process the images:

public struct function getDimensionsToEnlarge(
    required numeric imageWidth,
    required numeric imageHeight,
    required numeric minWidth,
    required numeric minHeight
    ) {
    LOCAL.Dimensions={
        width=-1,
        height=-1
        };

    if  (   ARGUMENTS.minHeight > 0
        &&  ARGUMENTS.minWidth > 0
        &&  ARGUMENTS.imageHeight < ARGUMENTS.minHeight
        &&  ARGUMENTS.imageWidth < ARGUMENTS.minWidth
        ) {
        LOCAL.Dimensions.width=ARGUMENTS.minWidth;
        LOCAL.Dimensions.height=ARGUMENTS.minHeight;
    }

    return LOCAL.Dimensions;
}

public struct function getDimensionsToShrink(
    required numeric imageWidth,
    required numeric imageHeight,
    required numeric maxWidth,
    required numeric maxHeight
    ) {
    LOCAL.Dimensions={
        width=-1,
        height=-1
        };

    if  (   ARGUMENTS.maxHeight > 0
        &&  ARGUMENTS.maxWidth > 0
        &&  (   ARGUMENTS.imageHeight > ARGUMENTS.maxHeight
            ||  ARGUMENTS.imageWidth > ARGUMENTS.maxWidth
            )
        ) {
        LOCAL.Dimensions.width=ARGUMENTS.maxWidth;
        LOCAL.Dimensions.height=ARGUMENTS.maxHeight;
    }

    return LOCAL.Dimensions;
}

public struct function getDimensionsToFit(
    required numeric imageWidth,
    required numeric imageHeight,
    required numeric minWidth,
    required numeric minHeight,
    required numeric maxWidth,
    required numeric maxHeight
    ) {
    LOCAL.Dimensions={
        width=-1,
        height=-1
        };

    LOCAL.Dimensions =
        getDimensionsToEnlarge(
            imageHeight=ARGUMENTS.imageHeight,
            imageWidth=ARGUMENTS.imageWidth,
            minWidth=ARGUMENTS.minWidth,
            minHeight=ARGUMENTS.minHeight
            );

    if (LOCAL.Dimensions.width < 0 && LOCAL.Dimensions.height < 0)
        LOCAL.Dimensions =
            getDimensionsToShrink(
                imageHeight=ARGUMENTS.imageHeight,
                imageWidth=ARGUMENTS.imageWidth,
                maxWidth=ARGUMENTS.maxWidth,
                maxHeight=ARGUMENTS.maxHeight
                );

    return LOCAL.Dimensions;
}

public any function scale(
    required any image,
    string action="fit",
    numeric minWidth=-1,
    numeric minHeight=-1,
    numeric maxWidth=-1,
    numeric maxHeight=-1
    ) {
    LOCAL.Dimensions={
            width=-1,
            height=-1
        };
    LOCAL.Image=Duplicate(ARGUMENTS.image);

    switch (ARGUMENTS.action) {
        case "shrink":
            LOCAL.Dimensions =
                getDimensionsToShrink(
                    imageHeight=LOCAL.Image.getHeight(),
                    imageWidth=LOCAL.Image.getWidth(),
                    maxWidth=ARGUMENTS.maxWidth,
                    maxHeight=ARGUMENTS.maxHeight
                );

            break;
        case "enlarge":
            LOCAL.Dimensions =
                getDimensionsToEnlarge(
                    imageHeight=LOCAL.Image.getHeight(),
                    imageWidth=LOCAL.Image.getWidth(),
                    minWidth=ARGUMENTS.minWidth,
                    minHeight=ARGUMENTS.minHeight
                );

            break;
        default:
            LOCAL.Dimensions =
                getDimensionsToFit(
                    imageHeight=LOCAL.Image.getHeight(),
                    imageWidth=LOCAL.Image.getWidth(),
                    minWidth=ARGUMENTS.minWidth,
                    minHeight=ARGUMENTS.minHeight,
                    maxWidth=ARGUMENTS.maxWidth,
                    maxHeight=ARGUMENTS.maxHeight
                );

            break;
    }

    if (LOCAL.Dimensions.width > 0 && LOCAL.Dimensions.height > 0) {
        ImageSetAntialiasing(LOCAL.Image, "on");

        ImageScaleToFit(
            LOCAL.Image,
            LOCAL.Dimensions.width,
            LOCAL.Dimensions.height
            );
    }

    return LOCAL.Image;
}

public void function createLargeThumbnail(required any image) {
    ImageWrite(
        scale(image=ARGUMENTS.image, maxHeight=500, maxWidth=700),
        getLargeImagePath(),
        0.75,
        true
        );

    return;
}

public void function createMediumThumbnail(required any image) {
    ImageWrite(
        scale(image=ARGUMENTS.image, maxHeight=300, maxWidth=300),
        getMediumImagePath(),
        0.75,
        true
        );

    return;
}

public void function createSmallThumbnail(required any image) {
    ImageWrite(
        scale(image=ARGUMENTS.image, maxHeight=50, maxWidth=50),
        getSmallImagePath(),
        0.75,
        true
        );

    return;
}

public void function createMobileThumbnail(required any image) {
    ImageWrite(
        scale(image=ARGUMENTS.image, maxHeight=50, maxWidth=300),
        getMobileImagePath(),
        0.75,
        true
        );

    return;
}

public void function createProductThumbnails(required any image) {
    createLargeThumbnail(argumentCollection=ARGUMENTS);
    createMediumThumbnail(argumentCollection=ARGUMENTS);
    createSmallThumbnail(argumentCollection=ARGUMENTS);
    createMobileThumbnail(argumentCollection=ARGUMENTS);

    return;
}

createProductThumbnails(ImageReadBase64(ARGUMENTS.ImageUpload));

Is there something that I need to do to initiate the garbage collection?

Appreciate your help!

rrk
  • 15,677
  • 4
  • 29
  • 45
Eric Belair
  • 10,574
  • 13
  • 75
  • 116
  • 1
    Which OS are you using? Have you considered using CFExecute with something like GraphicsMagick? I've found GM to be a lot faster and more compatible with various image formats & color palettes... no out-of-memory issues either. – James Moberg Nov 17 '20 at 21:34

1 Answers1

1

You are calling scale() four times (in each createXXXThumbnail()), effectively quadrupling your memory usage because you explicitly duplicate() the passed image object via arguments. You also call the native ImageScaleToFit() with the expensive default interpolation four times, which presumably creates another byte array internally. The innocent looking 20 MB can thus easily inflate up to 160 MB and more during processing.

Alex
  • 7,743
  • 1
  • 18
  • 38
  • Understood. However, it’s interesting that I frequently see the errors on the first call: createProductThumbnails(ImageReadBase64(ARGUMENTS.ImageUpload)); Suggestions on reducing the impact? – Eric Belair Nov 19 '20 at 03:07