imagetool/commands/resize.cpp (364 lines of code) (raw):

/* * MIT License * * Copyright (c) 2017 Twitter * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ #include "resize.h" #include "imagecore/utils/securemath.h" #include "imagecore/utils/mathutils.h" #include "imagecore/image/resizecrop.h" REGISTER_COMMAND("resize", ResizeCommand); bool parseOutputSize(const char* outputSize, unsigned int inputWidth, unsigned int inputHeight, unsigned int& targetWidth, unsigned int& targetHeight) { unsigned int w = 0; unsigned int h = 0; if( sscanf(outputSize, "%d%%x%d%%", &w, &h) == 2 ) { targetWidth = (inputWidth * w) / 100; targetHeight = (inputHeight * h) / 100; } else if( sscanf(outputSize, "%dx%d", &w, &h) == 2 ) { targetWidth = w; targetHeight = h; } else if( sscanf(outputSize, "%d%%", &w) == 1 ) { targetWidth = (inputWidth * w) / 100; targetHeight = (inputHeight * w) / 100; } else if( sscanf(outputSize, "%d", &w) == 1 ) { targetWidth = w; targetHeight = w; } return Image::validateSize(targetWidth, targetHeight); } bool parseBackgroundfillSize(const char* outputSize, unsigned int outputWidth, unsigned int outputHeight, unsigned int& targetWidth, unsigned int& targetHeight) { unsigned int w = 0; unsigned int h = 0; if( sscanf(outputSize, "%dx%d", &w, &h) == 2 ) { targetWidth = w; targetHeight = h; if( ( targetWidth < outputWidth) || ( targetHeight < outputHeight ) ) { return false; // can't have background smaller than the resized image } } return Image::validateSize(targetWidth, targetHeight); } bool parseBackfillColor(const char* backfillColor, unsigned char& backfillR, unsigned char& backfillG, unsigned char& backfillB ) { int rgb = 0; if( sscanf(backfillColor, "#%x", &rgb) == 1 ) { if( (rgb >= 0) && (rgb <= 0xFFFFFF) ) { backfillR = (unsigned char)((rgb >> 16) & 0xFF); backfillG = (unsigned char)((rgb >> 8) & 0xFF); backfillB = (unsigned char)(rgb & 0xFF); return true; } } return false; } // helper code to convert resize quality from string to enum struct ResizeQualityMapEntry { const char* m_String; EResizeQuality m_Enum; }; static ResizeQualityMapEntry resizeQualityMap[kResizeQuality_MAX] = { { "bilinear", kResizeQuality_Bilinear }, { "low", kResizeQuality_Low }, { "medium", kResizeQuality_Medium }, { "high", kResizeQuality_High }, { "highSharp", kResizeQuality_HighSharp } }; static EResizeQuality getResizeQuality(const char* resizeQuality) { for (uint32_t i = 0; i < kResizeQuality_MAX; i++) { if(strcmp(resizeQualityMap[i].m_String, resizeQuality) == 0) { return resizeQualityMap[i].m_Enum; } } // if can not find specified resize quality default to High return kResizeQuality_High; } Image* backfillImage(Image* srcImage, unsigned int width, unsigned int height, unsigned char r, unsigned char g, unsigned char b) { Image* image = Image::create(srcImage->getColorModel(), width, height, 0, 0); if( image != NULL ) { const unsigned int srcWidth = srcImage->getWidth(); const unsigned int widthDiff = width - srcWidth; const unsigned int srcHeight = srcImage->getHeight(); const unsigned int heightDiff = height - srcHeight; const unsigned int padLeft = widthDiff / 2; const unsigned int padTop = heightDiff / 2; const unsigned int padRight = width - srcWidth - padLeft; const unsigned int padBottom = height - srcHeight - padTop; if( image != NULL ) { srcImage->copyRect(image, 0, 0, padLeft, padTop, srcWidth, srcHeight); if( padTop > 0 ) { image->clearRect(0, 0, width, padTop, r, g, b, 255); } if( padBottom > 0 ) { image->clearRect(0, padTop + srcHeight, width, padBottom, r, g, b, 255); } if( padLeft > 0 ) { image->clearRect(0, padTop, padLeft, srcHeight, r, g, b, 255); } if( padRight > 0 ) { image->clearRect(padLeft + srcWidth, padTop, padRight, srcHeight, r, g, b, 255); } return image; } } return NULL; } ResizeCommand::ResizeCommand() { } ResizeCommand::~ResizeCommand() { } int ResizeCommand::run(const char** args, unsigned int numArgs) { if( numArgs < 3 ) { fprintf(stderr, "Usage: ImageTool resize <input> <output> <size> [-mode crop|fit|fill] [-gravity center|left|top|right|bottom] [-region <width>x<height>L<left_offset>T<top_offset>] [-mod N] [-filequality 0-100] [-resizequality 0-2] [-forcergb true|false] [-pad N,N,N]\n"); fprintf(stderr, "\te.g. ImageTool resize input.jpg output.jpg 1000x1000 -filequality 75\n"); return IMAGECORE_INVALID_USAGE; } int ret = open(args[0], args[1]); if (ret != IMAGECORE_SUCCESS) { return ret; } ret = performResize(args, numArgs); if (ret != IMAGECORE_SUCCESS) { return ret; } return close(); } int ResizeCommand::performResize(const char** args, unsigned int numArgs) { ImageReader* reader = ImageReader::create(m_Source); if( reader == NULL ) { fprintf(stderr, "error: unknown or corrupt image format for '%s'\n", m_InputFilePath); return IMAGECORE_INVALID_FORMAT; } const char* outputSize = args[2]; unsigned int parseWidth = reader->getOrientedWidth(); unsigned int parseHeight = reader->getOrientedHeight(); unsigned int outputWidth = 0; unsigned int outputHeight = 0; if( !parseOutputSize(outputSize, parseWidth, parseHeight, outputWidth, outputHeight) ) { fprintf(stderr, "error: bad size parameter\n"); delete reader; return IMAGECORE_INVALID_OUTPUT_SIZE; } bool shouldCrop = true; ECropGravity cropGravity = kGravityHeuristic; bool minAxis = false; EResizeQuality resizeQuality = kResizeQuality_High; unsigned int compressionQuality = 75; ImageRegion* cropRegion = NULL; bool allowYUV = true; bool allowUpsample = true; bool allowDownsample = true; EResizeMode resizeMode = kResizeMode_ExactCrop; const char* format = NULL; bool didSetMode = false; bool forceRGB = false; bool forceRLE = false; bool progressive = false; bool backfill = false; unsigned int backfillWidth = 0; unsigned int backfillHeight = 0; unsigned char backfillR = 0; unsigned char backfillG = 0; unsigned char backfillB = 0; unsigned int mod = 1; const char* writerArgNames[32]; const char* writerArgValues[32]; unsigned int numWriterArgs = 0; // Optional args. unsigned int numOptional = numArgs - 3; if( numOptional > 0 ) { unsigned int numPairs = numOptional / 2; for( unsigned int i = 0; i < numPairs; i++ ) { const char* argName = args[3 + i * 2 + 0]; const char* argValue = args[3 + i * 2 + 1]; if( strcmp(argName, "-crop") == 0 ) { shouldCrop = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-gravity") == 0 ) { if( strcmp(argValue, "center") == 0 ) { cropGravity = kGravityCenter; } else if( strcmp(argValue, "left") == 0 ) { cropGravity = kGravityLeft; } else if( strcmp(argValue, "top") == 0 ) { cropGravity = kGravityTop; } else if( strcmp(argValue, "right") == 0 ) { cropGravity = kGravityRight; } else if( strcmp(argValue, "bottom") == 0 ) { cropGravity = kGravityBottom; } } else if( strcmp(argName, "-region") == 0 ) { // because this flag causes a new ImageRegion to be allocated, // make sure to free the previous allocation in the case that // this is not the first -region flag. if( cropRegion != NULL ) { delete cropRegion; cropRegion = NULL; } if( (cropRegion = ImageRegion::fromString(argValue)) == NULL ) { fprintf(stderr, "error: invalid crop region given as '%s'\n", argValue); delete reader; return IMAGECORE_INVALID_USAGE; } if( cropRegion != NULL && ( !Image::validateSize(cropRegion->width(), cropRegion->height()) || SafeUAdd(cropRegion->left(), cropRegion->width()) > parseWidth || SafeUAdd(cropRegion->top(), cropRegion->height()) > parseHeight) ) { fprintf(stderr, "error: crop region not within image dimensions\n"); delete reader; return IMAGECORE_INVALID_OUTPUT_SIZE; } } else if( strcmp(argName, "-minaxis") == 0 ) { minAxis = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-resizequality") == 0 ) { resizeQuality = getResizeQuality(argValue); } else if( strcmp(argName, "-filequality") == 0 || strcmp(argName, "-quality") == 0 ) { compressionQuality = clamp(0, 100, atoi(argValue)); } else if( strcmp(argName, "-pad") == 0 ) { int ret = populateBuckets(argValue); if (ret != IMAGECORE_SUCCESS) { delete reader; return ret; } } else if( strcmp(argName, "-forcergb") == 0 ) { forceRGB = strcmp(argValue, "true") == 0; if( forceRGB ) { reader->setReadOptions(ImageReader::kReadOption_ApplyColorProfile); } } else if( strcmp(argName, "-yuvpath") == 0 ) { allowYUV = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-upsample") == 0 ) { allowUpsample = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-downsample") == 0 ) { allowDownsample = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-format") == 0 ) { format = argValue; } else if( strcmp(argName, "-progressive") == 0 ) { progressive = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-mode") == 0 ) { if( strcmp(argValue, "fit") == 0 ) { resizeMode = kResizeMode_AspectFit; } else if( strcmp(argValue, "fill") == 0 ) { resizeMode = kResizeMode_AspectFill; } else if( strcmp(argValue, "crop") == 0 ) { resizeMode = kResizeMode_ExactCrop; } else if( strcmp(argValue, "stretch") == 0 ) { resizeMode = kResizeMode_Stretch; } else { fprintf(stderr, "error: bad resize mode\n"); delete reader; return IMAGECORE_INVALID_USAGE; } didSetMode = true; } else if( strcmp(argName, "-mod") == 0 ) { mod = atoi(argValue); } else if( strcmp(argName, "-png:forceRLE") == 0 ) { forceRLE = strcmp(argValue, "true") == 0; } else if( strcmp(argName, "-backfillsize") == 0 ) { if ( !parseBackgroundfillSize(argValue, outputWidth, outputHeight, backfillWidth, backfillHeight) ) { fprintf(stderr, "error: bad backfill size\n"); delete reader; return IMAGECORE_INVALID_OUTPUT_SIZE; } backfill = true; } else if( strcmp(argName, "-backfillcolor") == 0 ) { if ( !parseBackfillColor(argValue, backfillR, backfillG, backfillB) ) { fprintf(stderr, "error: bad backfill color\n"); delete reader; return IMAGECORE_INVALID_COLOR; } } else if( strncmp(argName, "-encoder:", 9) == 0 ) { size_t argLen = strlen(argName) - 9; if( argLen > 0 ) { writerArgNames[numWriterArgs] = argName + 9; writerArgValues[numWriterArgs] = argValue; numWriterArgs++; } else { fprintf(stderr, "error: bad encoder argument\n"); delete reader; return IMAGECORE_INVALID_USAGE; } } } } if (!didSetMode) { // Legacy params. if (shouldCrop) { resizeMode = kResizeMode_ExactCrop; } else if (minAxis) { resizeMode = kResizeMode_AspectFit; } else { resizeMode = kResizeMode_AspectFill; } } EImageFormat outputFormat = ImageWriter::formatFromExtension(format != NULL ? format : args[1], reader->getFormat()); ResizeCropOperation resizeCrop; resizeCrop.setImageReader(reader); resizeCrop.setCropGravity(cropGravity); resizeCrop.setResizeQuality(resizeQuality); resizeCrop.setCropRegion(cropRegion); resizeCrop.setOutputSize(outputWidth, outputHeight); resizeCrop.setResizeMode(resizeMode); resizeCrop.setAllowUpsample(allowUpsample); resizeCrop.setAllowDownsample(allowDownsample); resizeCrop.setOutputMod(mod); if( backfill ) { resizeCrop.setBackgroundFillColor(backfillR, backfillG, backfillB); } // Reader and writer agree on a more optimal mutual color model and no backfill color is specified use native, // otherwise use default (rgb). EImageColorModel nativeColorModel = reader->getNativeColorModel(); if( ImageWriter::outputFormatSupportsColorModel(outputFormat, nativeColorModel) && !backfill ) { // Some further restrictions, the YUV path isn't optimized for anything but high quality. // Also, color profiles cannot be applied. if( Image::colorModelIsYUV(nativeColorModel) ) { if( allowYUV && resizeQuality >= kResizeQuality_High && !forceRGB ) { resizeCrop.setOutputColorModel(nativeColorModel); } } else { resizeCrop.setOutputColorModel(nativeColorModel); } } Image* resizedImage = NULL; int ret = resizeCrop.performResizeCrop(resizedImage); if( ret != IMAGECORE_SUCCESS ) { delete reader; return ret; } START_CLOCK(compress); ImageWriter* writer = ImageWriter::createWithFormat(outputFormat, m_Output); if (writer == NULL) { fprintf(stderr, "error: unable to create ImageWriter\n"); delete reader; return IMAGECORE_OUT_OF_MEMORY; } unsigned int writeOptions = 0; if( forceRGB ) { writeOptions |= ImageWriter::kWriteOption_WriteDefaultColorProfile; } else { writeOptions |= ImageWriter::kWriteOption_CopyColorProfile;; } if( progressive ) { writeOptions |= ImageWriter::kWriteOption_Progressive; } if( forceRLE ) { writeOptions |= ImageWriter::kWriteOption_ForcePNGRunLengthEncoding; } writer->setWriteOptions(writeOptions); // Allows certain formats to re-use information from the input image, like color profiles. writer->setSourceReader(reader); writer->setQuality(compressionQuality); if( !writer->applyExtraOptions(writerArgNames, writerArgValues, numWriterArgs) ) { fprintf(stderr, "error: unable to apply writer-specific options\n"); delete reader; delete writer; return IMAGECORE_INVALID_USAGE; } // handle backfill requests Image* backfilledImage = NULL; Image* finalImage; if( backfill ) { backfilledImage = backfillImage(resizedImage, backfillWidth, backfillHeight, backfillR, backfillG, backfillB); finalImage = backfilledImage; } else { finalImage = resizedImage; } if( !writer->writeImage(finalImage) ) { fprintf(stderr, "error: failed to compress image\n"); delete reader; delete writer; return IMAGECORE_WRITE_ERROR; } END_CLOCK(compress); delete backfilledImage; delete writer; delete reader; return IMAGECORE_SUCCESS; }