AnsweredAssumed Answered

Transforming PDF to high quality jpg - extending ImageMagick

Question asked by loftux Moderator on Feb 20, 2014
Latest reply on Feb 20, 2014 by loftux
I need to transform PDF (scanned documents) to jpg image for preview using thumbnails. The default output is barely readable.
I've found that you can get better output if you use
convert -density 300 source.pdf -quality 80 target.jpg


For the standard ImageMagick thirdparty subsystem you can add command options after "source.pdf", but I also need parameters <strong>before</strong>.

So I set out to extend the ImageMagick subsystem to allow for this parameter to be added.
Created two new classes with the added options, ExtendedImageMagickContentTransformerWorker and ExtendedImageTransformationOptions.
They ar mostly the same as the original classes, with the inputOptions added.
I wanted the ExtendedImageMagickContentTransformerWorker to support both ExtendedImageTransformationOptions and ImageTransformationOptions so that existing thumbnails didn't have to be redefined.
The bean for ImageMagick in thirdparty subsystem is changed, and it works well for existing thumbnails that use ImageTransformationOptions, but for my
thumbnail that use ExtendedImageTransformationOptions i get the error below.

<strong>Why do I get "No bean named '' is defined" error? Should i look in java source or bean?</strong>

When I change
<bean parent="defaultExtendedImageTransformationOptions">
to
<bean parent="defaultmageTransformationOptions">
it works again (and that still uses ExtendedImageMagickContentTransformerWorker).

EDIT: Sorry about the length of the post, but code sections ar supposed to display as blocks, that doesn't work.

<code title="Error when getting thumbnail">

org.springframework.beans.factory.NoSuchBeanDefinitionException - No bean named '' is defined

org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanDefinition(DefaultListableBeanFactory.java:527)
org.springframework.beans.factory.support.AbstractBeanFactory.getMergedLocalBeanDefinition(AbstractBeanFactory.java:1083)
org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:274)
org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:190)
org.springframework.context.support.AbstractApplicationContext.getBean(AbstractApplicationContext.java:1075)
org.alfresco.repo.action.ActionServiceImpl.directActionExecution(ActionServiceImpl.java:837)
org.alfresco.repo.action.ActionServiceImpl.executeActionImpl(ActionServiceImpl.java:738)
org.alfresco.repo.action.ActionServiceImpl.executeAction(ActionServiceImpl.java:572)
</code>
<code  title="Thumbnail bean definition" language="xml"  >

   <bean id="defaultExtendedImageTransformationOptions"
         class="se.loftux.repo.content.transform.magick.ExtendedImageTransformationOptions"
         abstract="true">
      <property name="timeoutMs" value="${system.thumbnail.definition.default.timeoutMs}" />
      <property name="readLimitTimeMs" value="${system.thumbnail.definition.default.readLimitTimeMs}" />
      <property name="maxSourceSizeKBytes" value="${system.thumbnail.definition.default.maxSourceSizeKBytes}" />
      <property name="readLimitKBytes" value="${system.thumbnail.definition.default.readLimitKBytes}" />
      <property name="pageLimit" value="${system.thumbnail.definition.default.pageLimit}" />
      <property name="maxPages" value="${system.thumbnail.definition.default.maxPages}" />
   </bean>

   <bean id="thumbnailTAMArkivOverlaypreview.registry" class="org.alfresco.repo.thumbnail.ThumbnailDefinitionSpringRegisterer">
      <property name="thumbnailRegistry" ref="thumbnailRegistry" />
      <property name="thumbnailDefinition">
         <bean id="thumbnailTAMArkivOverlaypreview" class="org.alfresco.repo.thumbnail.ThumbnailDefinition">
            <property name="name" value="overlay_preview" />
            <property name="mimetype" value="image/jpeg"/>
            <property name="transformationOptions">
               <bean parent="defaultExtendedImageTransformationOptions">
                  <property name="resizeOptions">
                     <bean class="org.alfresco.repo.content.transform.magick.ImageResizeOptions">
                        <property name="height" value="700"/>
                        <property name="width" value="630"/>
                        <property name="allowEnlargement" value="true" />
                        <property name="maintainAspectRatio" value="true" />
                        <property name="resizeToThumbnail" value="true" />
                     </bean>
                  </property>
                  <property name="commandOptions">
                     <value>-quality 80</value>
                  </property>
                  <property name="inputCommandOptions">
                     <value>-density 300</value>
                  </property>
               </bean>
            </property>
            <property name="placeHolderResourcePath" value="alfresco/thumbnail/thumbnail_placeholder_630.jpg" />
            <property name="runAs" value="System"/>
            <property name="failureHandlingOptions" ref="standardFailureOptions"/>
         </bean>
      </property>
   </bean>

</code>
<code  title="ExtendedImageMagickContentTransformerWorker" language="java"  >

package se.loftux.repo.content.transform.magick;

import java.io.File;
import java.util.HashMap;
import java.util.Map;

import org.alfresco.error.AlfrescoRuntimeException;
import org.alfresco.repo.content.MimetypeMap;
import org.alfresco.repo.content.transform.magick.ImageResizeOptions;
import org.alfresco.repo.content.transform.magick.ImageTransformationOptions;
import org.alfresco.repo.content.transform.magick.AbstractImageMagickContentTransformerWorker;
import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.CropSourceOptions;
import org.alfresco.service.cmr.repository.PagedSourceOptions;
import org.alfresco.service.cmr.repository.TransformationOptions;
import org.alfresco.util.exec.RuntimeExec;
import org.alfresco.util.exec.RuntimeExec.ExecutionResult;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import se.loftux.repo.content.transform.magick.ExtendedImageTransformationOptions;

/**
* Executes a statement to implement
*
* @author Derek Hulley
* Extended by peter on 2014-02-19.
*/
public class ExtendedImageMagickContentTransformerWorker extends AbstractImageMagickContentTransformerWorker
{
    /** Loftux input options variable name */
    private static final String KEY_INPUT_OPTIONS = "inputOptions";
    /** options variable name */
    private static final String KEY_OPTIONS = "options";
    /** source variable name */
    private static final String VAR_SOURCE = "source";
    /** target variable name */
    private static final String VAR_TARGET = "target";

    private static final Log logger = LogFactory.getLog(ExtendedImageMagickContentTransformerWorker.class);

    /** the system command executer */
    private RuntimeExec executer;

    /** the check command executer */
    private RuntimeExec checkCommand;

    /** the output from the check command */
    private String versionString;

    /**
     * Default constructor
     */
    public ExtendedImageMagickContentTransformerWorker()
    {
        // Intentionally empty
    }

    /**
     * @param executer the system command executer
     */
    public void setExecuter(RuntimeExec executer)
    {
        this.executer = executer;
    }


    /**
     * Sets the command that must be executed in order to retrieve version information from the converting executable
     * and thus test that the executable itself is present.
     *
     * @param checkCommand
     *            command executer to retrieve version information
     */
    public void setCheckCommand(RuntimeExec checkCommand)
    {
        this.checkCommand = checkCommand;
    }

    /**
     * Gets the version string captured from the check command.
     *
     * @return the version string
     */
    public String getVersionString()
    {
        return this.versionString;
    }


    /**
     * Checks for the JMagick and ImageMagick dependencies, using the common
     * {@link #transformInternal(java.io.File, java.io.File) transformation method} to check
     * that the sample image can be converted.
     */
    @Override
    public void afterPropertiesSet()
    {
        if (executer == null)
        {
            throw new AlfrescoRuntimeException("System runtime executer not set");
        }
        super.afterPropertiesSet();
        if (isAvailable())
        {
            try
            {
                // On some platforms / versions, the -version command seems to return an error code whilst still
                // returning output, so let's not worry about the exit code!
                ExecutionResult result = this.checkCommand.execute();
                this.versionString = result.getStdOut().trim();
            }
            catch (Throwable e)
            {
                setAvailable(false);
                logger.error(getClass().getSimpleName() + " not available: "
                        + (e.getMessage() != null ? e.getMessage() : ""));
                // debug so that we can trace the issue if required
                logger.debug(e);
            }

        }
    }

    /**
     * Transform the image content from the source file to the target file
     */
    @Override
    protected void transformInternal(File sourceFile, String sourceMimetype,
                                     File targetFile, String targetMimetype, TransformationOptions options) throws Exception
    {
        Map<String, String> properties = new HashMap<String, String>(5);
        // set properties
        if (options instanceof ImageTransformationOptions)
        {
            ImageTransformationOptions imageOptions = (ImageTransformationOptions)options;
            CropSourceOptions cropOptions = imageOptions.getSourceOptions(CropSourceOptions.class);
            ImageResizeOptions resizeOptions = imageOptions.getResizeOptions();
            String commandOptions = imageOptions.getCommandOptions();
            if (commandOptions == null)
            {
                commandOptions = "";
            }
            if (imageOptions.isAutoOrient())
            {
                commandOptions = commandOptions + " -auto-orient";
            }
            if (cropOptions != null)
            {
                commandOptions = commandOptions + " " + getImageCropCommandOptions(cropOptions);
            }
            if (resizeOptions != null)
            {
                commandOptions = commandOptions + " " + getImageResizeCommandOptions(resizeOptions);
            }
            properties.put(KEY_OPTIONS, commandOptions);
        }
        // Loftux - Check for
        if (options instanceof ExtendedImageTransformationOptions)
        {
            ExtendedImageTransformationOptions extendedImageOptions = (ExtendedImageTransformationOptions)options;
            String inputCommandOptions = extendedImageOptions.getInputCommandOptions();
            CropSourceOptions cropOptions = extendedImageOptions.getSourceOptions(CropSourceOptions.class);
            ImageResizeOptions resizeOptions = extendedImageOptions.getResizeOptions();
            String commandOptions = extendedImageOptions.getCommandOptions();
            if (inputCommandOptions == null)
            {
                inputCommandOptions = "";
            }
            if (commandOptions == null)
            {
                commandOptions = "";
            }
            if (extendedImageOptions.isAutoOrient())
            {
                commandOptions = commandOptions + " -auto-orient";
            }
            if (cropOptions != null)
            {
                commandOptions = commandOptions + " " + getImageCropCommandOptions(cropOptions);
            }
            if (resizeOptions != null)
            {
                commandOptions = commandOptions + " " + getImageResizeCommandOptions(resizeOptions);
            }
            properties.put(KEY_OPTIONS, commandOptions);
            properties.put(KEY_INPUT_OPTIONS, inputCommandOptions);
        }
        else
        {
            properties.put(KEY_INPUT_OPTIONS, "");
        }
        properties.put(VAR_SOURCE, sourceFile.getAbsolutePath() +
                getSourcePageRange(options, sourceMimetype, targetMimetype));
        properties.put(VAR_TARGET, targetFile.getAbsolutePath());

        // execute the statement
        long timeoutMs = options.getTimeoutMs();
        RuntimeExec.ExecutionResult result = executer.execute(properties, timeoutMs);
        if (result.getExitValue() != 0 && result.getStdErr() != null && result.getStdErr().length() > 0)
        {
            throw new ContentIOException("Failed to perform ImageMagick transformation: \n" + result);
        }
        // success
        if (logger.isDebugEnabled())
        {
            logger.debug("Loftux Extended ImageMagic executed successfully: \n" + executer);
        }
    }

    /**
     * Gets the imagemagick command string for the image crop options provided
     *
     * @param imageResizeOptions    image resize options
     * @return String               the imagemagick command options
     */
    private String getImageCropCommandOptions(CropSourceOptions cropOptions)
    {
        StringBuilder builder = new StringBuilder(32);
        String gravity = cropOptions.getGravity();
        if(gravity!=null)
        {
            builder.append("-gravity ");
            builder.append(gravity);
            builder.append(" ");
        }
        builder.append("-crop ");
        int width = cropOptions.getWidth();
        if (width > -1)
        {
            builder.append(width);
        }

        int height = cropOptions.getHeight();
        if (height > -1)
        {
            builder.append("x");
            builder.append(height);
        }

        if (cropOptions.isPercentageCrop())
        {
            builder.append("%");
        }
        appendOffset(builder, cropOptions.getXOffset());
        appendOffset(builder, cropOptions.getYOffset());
        builder.append(" +repage");
        return builder.toString();
    }

    /**
     * @param builder
     * @param xOffset
     */
    private void appendOffset(StringBuilder builder, int xOffset)
    {
        if(xOffset>=0)
        {
            builder.append("+");
        }
        builder.append(xOffset);
    }

    /**
     * Gets the imagemagick command string for the image resize options provided
     *
     * @param imageResizeOptions    image resize options
     * @return String               the imagemagick command options
     */
    private String getImageResizeCommandOptions(ImageResizeOptions imageResizeOptions)
    {
        StringBuilder builder = new StringBuilder(32);

        // These are ImageMagick options. See http://www.imagemagick.org/script/command-line-processing.php#geometry for details.
        if (imageResizeOptions.isResizeToThumbnail() == true)
        {
            builder.append("-thumbnail ");
        }
        else
        {
            builder.append("-resize ");
        }

        if (imageResizeOptions.getWidth() > -1)
        {
            builder.append(imageResizeOptions.getWidth());
        }

        if (imageResizeOptions.getHeight() > -1)
        {
            builder.append("x");
            builder.append(imageResizeOptions.getHeight());
        }

        if (imageResizeOptions.isPercentResize() == true)
        {
            builder.append("%");
        }
        // ALF-7308. Disallow the enlargement of small images e.g. within imgpreview thumbnail.
        if (!imageResizeOptions.getAllowEnlargement())
        {
            builder.append(">");
        }

        if (imageResizeOptions.isMaintainAspectRatio() == false)
        {
            builder.append("!");
        }

        return builder.toString();
    }

    /**
     * Determines whether or not a single page range is required for the given source and target mimetypes.
     *
     * @param sourceMimetype
     * @param targetMimetype
     * @return whether or not a page range must be specified for the transformer to read the target files
     */
    private boolean isSingleSourcePageRangeRequired(String sourceMimetype, String targetMimetype)
    {
        // Need a page source if we're transforming from PDF or TIFF to an image other than TIFF
        // or from PSD
        return ((sourceMimetype.equals(MimetypeMap.MIMETYPE_PDF) ||
                sourceMimetype.equals(MimetypeMap.MIMETYPE_IMAGE_TIFF)) &&
                ((!targetMimetype.equals(MimetypeMap.MIMETYPE_IMAGE_TIFF)
                        && targetMimetype.contains(MIMETYPE_IMAGE_PREFIX)) ||
                        targetMimetype.equals(MimetypeMap.MIMETYPE_APPLICATION_PHOTOSHOP) ||
                        targetMimetype.equals(MimetypeMap.MIMETYPE_APPLICATION_EPS)) ||
                sourceMimetype.equals(MimetypeMap.MIMETYPE_APPLICATION_PHOTOSHOP));
    }

    /**
     * Gets the page range from the source to use in the command line.
     *
     * @param options the transformation options
     * @param sourceMimetype the source mimetype
     * @param targetMimetype the target mimetype
     * @return the source page range for the command line
     */
    private String getSourcePageRange(TransformationOptions options, String sourceMimetype, String targetMimetype)
    {
        // Check for PagedContentSourceOptions in the options
        if (options instanceof ImageTransformationOptions)
        {
            ImageTransformationOptions imageOptions = (ImageTransformationOptions) options;
            PagedSourceOptions pagedSourceOptions = imageOptions.getSourceOptions(PagedSourceOptions.class);
            if (pagedSourceOptions != null)
            {
                if (pagedSourceOptions.getStartPageNumber() != null &&
                        pagedSourceOptions.getEndPageNumber() != null)
                {
                    if (pagedSourceOptions.getStartPageNumber().equals(pagedSourceOptions.getEndPageNumber()))
                    {
                        return "[" + (pagedSourceOptions.getStartPageNumber() - 1) + "]";
                    }
                    else
                    {
                        if (isSingleSourcePageRangeRequired(sourceMimetype, targetMimetype))
                        {
                            throw new AlfrescoRuntimeException(
                                    "A single page is required for targets of type " + targetMimetype);
                        }
                        return "[" + (pagedSourceOptions.getStartPageNumber() - 1) +
                                "-" + (pagedSourceOptions.getEndPageNumber() - 1) + "]";
                    }
                }
                else
                {
                    // TODO specified start to end of doc and start of doc to specified end not yet supported
                    // Just grab a single page specified by either start or end
                    if (pagedSourceOptions.getStartPageNumber() != null)
                        return "[" + (pagedSourceOptions.getStartPageNumber() - 1) + "]";
                    if (pagedSourceOptions.getEndPageNumber() != null)
                        return "[" + (pagedSourceOptions.getEndPageNumber() - 1) + "]";
                }
            }
        }
        if (options instanceof ExtendedImageTransformationOptions)
        {
            ExtendedImageTransformationOptions extendedImageOptions = (ExtendedImageTransformationOptions) options;
            PagedSourceOptions pagedSourceOptions = extendedImageOptions.getSourceOptions(PagedSourceOptions.class);
            if (pagedSourceOptions != null)
            {
                if (pagedSourceOptions.getStartPageNumber() != null &&
                        pagedSourceOptions.getEndPageNumber() != null)
                {
                    if (pagedSourceOptions.getStartPageNumber().equals(pagedSourceOptions.getEndPageNumber()))
                    {
                        return "[" + (pagedSourceOptions.getStartPageNumber() - 1) + "]";
                    }
                    else
                    {
                        if (isSingleSourcePageRangeRequired(sourceMimetype, targetMimetype))
                        {
                            throw new AlfrescoRuntimeException(
                                    "A single page is required for targets of type " + targetMimetype);
                        }
                        return "[" + (pagedSourceOptions.getStartPageNumber() - 1) +
                                "-" + (pagedSourceOptions.getEndPageNumber() - 1) + "]";
                    }
                }
                else
                {
                    // TODO specified start to end of doc and start of doc to specified end not yet supported
                    // Just grab a single page specified by either start or end
                    if (pagedSourceOptions.getStartPageNumber() != null)
                        return "[" + (pagedSourceOptions.getStartPageNumber() - 1) + "]";
                    if (pagedSourceOptions.getEndPageNumber() != null)
                        return "[" + (pagedSourceOptions.getEndPageNumber() - 1) + "]";
                }
            }
        }
        if (options.getPageLimit() == 1 || isSingleSourcePageRangeRequired(sourceMimetype, targetMimetype))
        {
            return "[0]";
        }
        else
        {
            return "";
        }
    }
}

</code>

<code  title="ExtendedImageTransformationOptions" language="java"  >

package se.loftux.repo.content.transform.magick;

import java.util.HashMap;
import java.util.Map;

import org.alfresco.repo.content.transform.magick.ImageResizeOptions;
import org.alfresco.service.cmr.repository.TransformationOptions;
import org.alfresco.service.cmr.repository.TransformationSourceOptions;

/**
* Image transformation options
*
* @author Roy Wetherall
* Extended by peter on 2014-02-18.
*/
public class ExtendedImageTransformationOptions extends TransformationOptions
{
    // Loftux - New inputCommand options
    public static final String OPT_INPUT_COMMAND_OPTIONS = "inputCommandOptions";
    public static final String OPT_COMMAND_OPTIONS = "commandOptions";
    public static final String OPT_IMAGE_RESIZE_OPTIONS = "imageResizeOptions";
    public static final String OPT_IMAGE_AUTO_ORIENTATION = "imageAutoOrient";


    /** Loftux inputCommand string options, provided to set input file options */
    private String inputCommandOptions = "";

    /** Command string options, provided for backward compatibility */
    private String commandOptions = "";

    /** Image resize options */
    private ImageResizeOptions resizeOptions;

    private boolean autoOrient = true;

    /**
     * Set the pre-command string options
     * Loftux
     * @param inputCommandOptions    the command string options
     */
    public void setInputCommandOptions(String inputCommandOptions)
    {
        this.inputCommandOptions = inputCommandOptions;
    }

    /**
     * Get the pre-command string options
     * Loftux
     * @return  String  the command string options
     */
    public String getInputCommandOptions()
    {
        return inputCommandOptions;
    }

    /**
     * Set the command string options
     *
     * @param commandOptions    the command string options
     */
    public void setCommandOptions(String commandOptions)
    {
        this.commandOptions = commandOptions;
    }

    /**
     * Get the command string options
     *
     * @return  String  the command string options
     */
    public String getCommandOptions()
    {
        return commandOptions;
    }

    /**
     * Set the image resize options
     *
     * @param resizeOptions image resize options
     */
    public void setResizeOptions(ImageResizeOptions resizeOptions)
    {
        this.resizeOptions = resizeOptions;
    }

    /**
     * Get the image resize options
     *
     * @return  ImageResizeOptions  image resize options
     */
    public ImageResizeOptions getResizeOptions()
    {
        return resizeOptions;
    }

    @Override
    public String toString()
    {
        StringBuilder builder = new StringBuilder();
        builder.append("ExtendedImageTransformationOptions [inputCommandOptions=").append(this.inputCommandOptions)
                .append(", commandOptions=").append(this.commandOptions)
                .append(", resizeOptions=").append(this.resizeOptions)
                .append(", autoOrient=").append(this.autoOrient).append("]");
        if (getSourceOptionsList() != null)
        {
            builder.append(", sourceOptions={ ");
            int i = 0;
            for (TransformationSourceOptions sourceOptions : getSourceOptionsList())
            {
                builder.append((i != 0) ? " , ": "");
                builder.append(sourceOptions.getClass().getSimpleName())
                        .append(sourceOptions.toString());
                i++;
            }
            builder.append("} ");
        }
        builder.append("]");
        return builder.toString();
    }

    /**
     * Overrides the base class implementation to add our options
     */
    @Override
    public Map<String, Object> toMap()
    {
        Map<String, Object> baseProps = super.toMap();
        Map<String, Object> props = new HashMap<String, Object>(baseProps);
        props.put(OPT_INPUT_COMMAND_OPTIONS, inputCommandOptions);
        props.put(OPT_COMMAND_OPTIONS, commandOptions);
        props.put(OPT_IMAGE_RESIZE_OPTIONS, resizeOptions);
        props.put(OPT_IMAGE_AUTO_ORIENTATION, autoOrient);
        return props;
    }

    /**
     * @return Will the image be automatically oriented(rotated) based on the EXIF "Orientation" data.
     * Defaults to TRUE
     */
    public boolean isAutoOrient()
    {
        return this.autoOrient;
    }

    /**
     * @param autoOrient automatically orient (rotate) based on the EXIF "Orientation" data
     */
    public void setAutoOrient(boolean autoOrient)
    {
        this.autoOrient = autoOrient;
    }

    @Override
    public void copyFrom(TransformationOptions origOptions) {
        super.copyFrom(origOptions);
        if (origOptions != null)
        {
            if (origOptions instanceof ExtendedImageTransformationOptions)
            {
                // Clone ExtendedImageTransformationOptions
                this.setInputCommandOptions(((ExtendedImageTransformationOptions) origOptions).getInputCommandOptions());
                this.setCommandOptions(((ExtendedImageTransformationOptions) origOptions).getCommandOptions());
                this.setResizeOptions(((ExtendedImageTransformationOptions) origOptions).getResizeOptions());
                this.setAutoOrient(((ExtendedImageTransformationOptions) origOptions).isAutoOrient());
            }
        }
    }

}

</code>

Outcomes