/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.apache.nifi.processors.image; import java.awt.Graphics2D; import java.awt.Image; import java.awt.image.BufferedImage; import java.io.BufferedInputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import org.apache.nifi.annotation.behavior.EventDriven; import org.apache.nifi.annotation.behavior.InputRequirement; import org.apache.nifi.annotation.behavior.InputRequirement.Requirement; import org.apache.nifi.annotation.behavior.SupportsBatching; import org.apache.nifi.annotation.documentation.CapabilityDescription; import org.apache.nifi.annotation.documentation.Tags; import org.apache.nifi.components.AllowableValue; import org.apache.nifi.components.PropertyDescriptor; import org.apache.nifi.flowfile.FlowFile; import org.apache.nifi.processor.AbstractProcessor; import org.apache.nifi.processor.ProcessContext; import org.apache.nifi.processor.ProcessSession; import org.apache.nifi.processor.Relationship; import org.apache.nifi.processor.exception.ProcessException; import org.apache.nifi.processor.io.StreamCallback; import org.apache.nifi.processor.util.StandardValidators; import org.apache.nifi.util.StopWatch; @EventDriven @SupportsBatching @InputRequirement(Requirement.INPUT_REQUIRED) @Tags({ "resize", "image", "jpg", "jpeg", "png", "bmp", "wbmp", "gif" }) @CapabilityDescription("Resizes an image to user-specified dimensions. This Processor uses the image codecs registered with the " + "environment that NiFi is running in. By default, this includes JPEG, PNG, BMP, WBMP, and GIF images.") public class ResizeImage extends AbstractProcessor { static final AllowableValue RESIZE_DEFAULT = new AllowableValue("Default", "Default", "Use the default algorithm"); static final AllowableValue RESIZE_FAST = new AllowableValue("Scale Fast", "Scale Fast", "Emphasize speed of the scaling over smoothness"); static final AllowableValue RESIZE_SMOOTH = new AllowableValue("Scale Smooth", "Scale Smooth", "Emphasize smoothness of the scaling over speed"); static final AllowableValue RESIZE_REPLICATE = new AllowableValue("Replicate Scale Filter", "Replicate Scale Filter", "Use the Replicate Scale Filter algorithm"); static final AllowableValue RESIZE_AREA_AVERAGING = new AllowableValue("Area Averaging", "Area Averaging", "Use the Area Averaging scaling algorithm"); static final PropertyDescriptor IMAGE_WIDTH = new PropertyDescriptor.Builder() .name("Image Width (in pixels)") .description("The desired number of pixels for the image's width") .required(true) .expressionLanguageSupported(true) .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) .build(); static final PropertyDescriptor IMAGE_HEIGHT = new PropertyDescriptor.Builder() .name("Image Height (in pixels)") .description("The desired number of pixels for the image's height") .required(true) .expressionLanguageSupported(true) .addValidator(StandardValidators.POSITIVE_INTEGER_VALIDATOR) .build(); static final PropertyDescriptor SCALING_ALGORITHM = new PropertyDescriptor.Builder() .name("Scaling Algorithm") .description("Specifies which algorithm should be used to resize the image") .required(true) .allowableValues(RESIZE_DEFAULT, RESIZE_FAST, RESIZE_SMOOTH, RESIZE_REPLICATE, RESIZE_AREA_AVERAGING) .defaultValue(RESIZE_DEFAULT.getValue()) .build(); static final Relationship REL_SUCCESS = new Relationship.Builder() .name("success") .description("A FlowFile is routed to this relationship if it is successfully resized") .build(); static final Relationship REL_FAILURE = new Relationship.Builder() .name("failure") .description("A FlowFile is routed to this relationship if it is not in the specified format") .build(); @Override protected List<PropertyDescriptor> getSupportedPropertyDescriptors() { final List<PropertyDescriptor> properties = new ArrayList<>(); properties.add(IMAGE_WIDTH); properties.add(IMAGE_HEIGHT); properties.add(SCALING_ALGORITHM); return properties; } @Override public Set<Relationship> getRelationships() { final Set<Relationship> relationships = new HashSet<>(); relationships.add(REL_SUCCESS); relationships.add(REL_FAILURE); return relationships; } @Override public void onTrigger(final ProcessContext context, final ProcessSession session) throws ProcessException { FlowFile flowFile = session.get(); if (flowFile == null) { return; } final int width, height; try { width = context.getProperty(IMAGE_WIDTH).evaluateAttributeExpressions(flowFile).asInteger(); height = context.getProperty(IMAGE_HEIGHT).evaluateAttributeExpressions(flowFile).asInteger(); } catch (final NumberFormatException nfe) { getLogger().error("Failed to resize {} due to {}", new Object[] { flowFile, nfe }); session.transfer(flowFile, REL_FAILURE); return; } final String algorithm = context.getProperty(SCALING_ALGORITHM).getValue(); final int hints; if (algorithm.equalsIgnoreCase(RESIZE_DEFAULT.getValue())) { hints = Image.SCALE_DEFAULT; } else if (algorithm.equalsIgnoreCase(RESIZE_FAST.getValue())) { hints = Image.SCALE_FAST; } else if (algorithm.equalsIgnoreCase(RESIZE_SMOOTH.getValue())) { hints = Image.SCALE_SMOOTH; } else if (algorithm.equalsIgnoreCase(RESIZE_REPLICATE.getValue())) { hints = Image.SCALE_REPLICATE; } else if (algorithm.equalsIgnoreCase(RESIZE_AREA_AVERAGING.getValue())) { hints = Image.SCALE_AREA_AVERAGING; } else { throw new AssertionError("Invalid Scaling Algorithm: " + algorithm); } final StopWatch stopWatch = new StopWatch(true); try { flowFile = session.write(flowFile, new StreamCallback() { @Override public void process(final InputStream rawIn, final OutputStream out) throws IOException { try (final BufferedInputStream in = new BufferedInputStream(rawIn)) { final ImageInputStream iis = ImageIO.createImageInputStream(in); if (iis == null) { throw new ProcessException("FlowFile is not in a valid format"); } final Iterator<ImageReader> readers = ImageIO.getImageReaders(iis); if (!readers.hasNext()) { throw new ProcessException("FlowFile is not in a valid format"); } final ImageReader reader = readers.next(); final String formatName = reader.getFormatName(); reader.setInput(iis, true); final BufferedImage image = reader.read(0); final Image scaledImage = image.getScaledInstance(width, height, hints); final BufferedImage scaledBufferedImg; if (scaledImage instanceof BufferedImage) { scaledBufferedImg = (BufferedImage) scaledImage; } else { scaledBufferedImg = new BufferedImage(scaledImage.getWidth(null), scaledImage.getHeight(null), image.getType()); final Graphics2D graphics = scaledBufferedImg.createGraphics(); try { graphics.drawImage(scaledImage, 0, 0, null); } finally { graphics.dispose(); } } ImageIO.write(scaledBufferedImg, formatName, out); } } }); session.getProvenanceReporter().modifyContent(flowFile, stopWatch.getElapsed(TimeUnit.MILLISECONDS)); session.transfer(flowFile, REL_SUCCESS); } catch (final ProcessException pe) { getLogger().error("Failed to resize {} due to {}", new Object[] { flowFile, pe }); session.transfer(flowFile, REL_FAILURE); } } }