MediaUtil.java

/*
 * Copyright (C) 2000 - 2024 Silverpeas
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * As a special exception to the terms and conditions of version 3.0 of
 * the GPL, you may redistribute this Program in connection with Free/Libre
 * Open Source Software ("FLOSS") applications as described in Silverpeas's
 * FLOSS exception. You should have received a copy of the text describing
 * the FLOSS exception, and it is also available here:
 * "http://www.silverpeas.org/docs/core/legal/floss_exception.html"
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
 */
package org.silverpeas.components.gallery;

import org.apache.commons.fileupload.FileItem;
import org.apache.commons.io.FileUtils;
import org.silverpeas.components.gallery.constant.MediaMimeType;
import org.silverpeas.components.gallery.constant.MediaResolution;
import org.silverpeas.components.gallery.constant.MediaType;
import org.silverpeas.components.gallery.media.DrewMediaMetadataExtractor;
import org.silverpeas.components.gallery.media.MediaMetadataException;
import org.silverpeas.components.gallery.media.MediaMetadataExtractor;
import org.silverpeas.components.gallery.model.GalleryRuntimeException;
import org.silverpeas.components.gallery.model.InternalMedia;
import org.silverpeas.components.gallery.model.Media;
import org.silverpeas.components.gallery.model.MediaPK;
import org.silverpeas.components.gallery.model.MetaData;
import org.silverpeas.components.gallery.model.Photo;
import org.silverpeas.components.gallery.model.Sound;
import org.silverpeas.components.gallery.model.Video;
import org.silverpeas.core.io.media.Definition;
import org.silverpeas.core.io.media.MetadataExtractor;
import org.silverpeas.core.io.media.image.ImageTool;
import org.silverpeas.core.io.media.image.option.AbstractImageToolOption;
import org.silverpeas.core.io.media.image.option.AnchoringPosition;
import org.silverpeas.core.io.media.image.option.DimensionOption;
import org.silverpeas.core.io.media.image.option.OrientationOption;
import org.silverpeas.core.io.media.image.option.WatermarkImageOption;
import org.silverpeas.core.io.media.image.option.WatermarkTextOption;
import org.silverpeas.core.io.media.video.VideoThumbnailExtractor;
import org.silverpeas.core.notification.message.MessageManager;
import org.silverpeas.core.process.io.file.FileHandler;
import org.silverpeas.core.process.io.file.HandledFile;
import org.silverpeas.kernel.util.StringUtil;
import org.silverpeas.core.util.file.FileUtil;
import org.silverpeas.kernel.logging.SilverLogger;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;

import static org.apache.commons.io.FilenameUtils.getExtension;
import static org.apache.commons.io.filefilter.FileFilterUtils.*;
import static org.silverpeas.components.gallery.constant.MediaResolution.*;
import static org.silverpeas.core.io.media.image.ImageInfoType.HEIGHT_IN_PIXEL;
import static org.silverpeas.core.io.media.image.ImageInfoType.WIDTH_IN_PIXEL;
import static org.silverpeas.core.io.media.image.ImageToolDirective.GEOMETRY_SHRINK;
import static org.silverpeas.core.io.media.image.ImageToolDirective.PREVIEW_WORK;
import static org.silverpeas.core.io.media.video.ThumbnailPeriod.VIDEO_THUMBNAIL_FILE_EXTENSION;
import static org.silverpeas.core.io.media.video.ThumbnailPeriod.VIDEO_THUMBNAIL_FILE_PREFIX;
import static org.silverpeas.kernel.util.StringUtil.defaultStringIfNotDefined;
import static org.silverpeas.kernel.util.StringUtil.isDefined;

public class MediaUtil {

  private MediaUtil() {
  }

  private static void pasteFile(final HandledFile fromFile, final HandledFile toFile,
      final boolean cut) {
    if (fromFile.exists()) {
      try {
        if (cut) {
          fromFile.moveFile(toFile);
        } else {
          fromFile.copyFile(toFile);
        }
      } catch (final Exception e) {
        SilverLogger.getLogger(MediaUtil.class).error(
            "Unable to copy file : fromImage = " + fromFile.getFile().getPath() + ", toImage = " +
                toFile.getFile().getPath(), e);
      }
    }
  }

  /**
   * Gets a handled file.
   * @param fileHandler the current session file handler
   * @param media the original media file to get.
   * @return the handled file
   */
  private static HandledFile getHandledFile(FileHandler fileHandler, InternalMedia media) {
    if (StringUtil.isNotDefined(media.getFileName())) {
      throw new IllegalArgumentException("media.getFilename() must return a defined name");
    }
    return fileHandler.getHandledFile(Media.BASE_PATH, media.getComponentInstanceId(),
        media.getWorkspaceSubFolderName(), media.getFileName());
  }

  private static void setMetaData(final FileHandler fileHandler, final Photo photo,
      final String lang) throws MediaMetadataException, IOException {
    if (MediaMimeType.JPG == photo.getFileMimeType()) {
      final HandledFile handledFile = fileHandler
          .getHandledFile(Media.BASE_PATH, photo.getInstanceId(), photo.getWorkspaceSubFolderName(),
              photo.getFileName());
      if (handledFile.exists()) {
        try {
          final MediaMetadataExtractor extractor = new DrewMediaMetadataExtractor(photo.
              getInstanceId());
          extractor.extractImageExifMetaData(handledFile.getFile(), lang)
              .forEach(photo::addMetaData);
          extractor.extractImageIptcMetaData(handledFile.getFile(), lang)
              .forEach(photo::addMetaData);
        } catch (UnsupportedEncodingException e) {
          SilverLogger.getLogger(MediaUtil.class).silent(e)
              .error("Bad metadata encoding in image " + photo.getTitle() + ": " + e.getMessage());
        }
      }
    }
  }

  /**
   * Saves uploaded sound file on file system
   * @param fileHandler the current session file handler
   * @param sound the current sound media
   * @param fileItem the current uploaded sound
   * @throws Exception
   */
  public static synchronized void processSound(final FileHandler fileHandler, Sound sound,
      final FileItem fileItem) throws Exception {
    if (fileItem != null) {
      String name = fileItem.getName();
      if (name != null) {
        try {
          sound.setFileName(StringUtil.normalize(FileUtil.getFilename(name)));
          final HandledFile handledSoundFile = getHandledFile(fileHandler, sound);
          handledSoundFile.copyInputStreamToFile(fileItem.getInputStream());
          new SoundProcess(handledSoundFile, sound).process();
        } finally {
          fileItem.delete();
        }
      }
    }
  }

  /**
   * Saves uploaded sound file on file system (In case of drag And Drop upload)
   * @param fileHandler the current session file handler
   * @param sound the current sound media
   * @param uploadedFile the current uploaded sound
   * @throws Exception
   */
  public static synchronized void processSound(final FileHandler fileHandler, Sound sound,
      final File uploadedFile) throws Exception {
    if (uploadedFile != null) {
      try {
        sound.setFileName(StringUtil.normalize(uploadedFile.getName()));
        final HandledFile handledSoundFile = getHandledFile(fileHandler, sound);
        fileHandler.copyFile(uploadedFile, handledSoundFile);
        new SoundProcess(handledSoundFile, sound).process();
      } finally {
        FileUtils.deleteQuietly(uploadedFile);
      }
    }
  }

  /**
   * Saves uploaded video file on file system
   * @param fileHandler the current session file handler
   * @param video the current video media
   * @param fileItem the current uploaded video
   * @throws Exception
   */
  public static synchronized void processVideo(final FileHandler fileHandler, Video video,
      final FileItem fileItem) throws Exception {
    if (fileItem != null) {
      String name = fileItem.getName();
      if (name != null) {
        try {
          video.setFileName(StringUtil.normalize(FileUtil.getFilename(name)));
          final HandledFile handledVideoFile = getHandledFile(fileHandler, video);
          handledVideoFile.copyInputStreamToFile(fileItem.getInputStream());
          new VideoProcess(handledVideoFile, video).process();
        } finally {
          fileItem.delete();
        }
      }
    }
  }

  /**
   * Saves uploaded video file on file system (In case of drag And Drop upload)
   * @param fileHandler the current session file handler
   * @param video the current video media
   * @param uploadedFile the current uploaded video
   * @throws Exception
   */
  public static synchronized void processVideo(final FileHandler fileHandler, Video video,
      final File uploadedFile) throws Exception {
    if (uploadedFile != null) {
      try {
        video.setFileName(StringUtil.normalize(uploadedFile.getName()));
        final HandledFile handledVideoFile = getHandledFile(fileHandler, video);
        fileHandler.copyFile(uploadedFile, handledVideoFile);
        new VideoProcess(handledVideoFile, video).process();
      } finally {
        FileUtils.deleteQuietly(uploadedFile);
      }
    }
  }

  /**
   * Saves uploaded photo file on file system with associated thumbnails and watermarks.
   * @param fileHandler the current session file handler
   * @param photo the photo media
   * @param image the image to register
   * @param watermark if watermark must be handled, the data are represented by this
   * {@link Watermark} instance.
   * @throws Exception on technical error.
   */
  public static synchronized void processPhoto(final FileHandler fileHandler, final Photo photo,
      final FileItem image, final Watermark watermark) throws Exception {
    if (image != null) {
      String name = image.getName();
      if (name != null) {
        try {
          photo.setFileName(StringUtil.normalize(image.getName()));
          final HandledFile handledImageFile = getHandledFile(fileHandler, photo);
          handledImageFile.copyInputStreamToFile(image.getInputStream());
          new PhotoProcess(handledImageFile, photo, watermark).process();
        } finally {
          image.delete();
        }
      }
    }
  }

  /**
   * Saves uploaded photo file on file system with associated thumbnails and watermarks. (In case
   * of
   * drag And Drop upload)
   * @param fileHandler the current session file handler
   * @param photo the photo media
   * @param image the image to register
   * @param watermark if watermark must be handled, the data are represented by this
   * {@link Watermark} instance.
   * @throws Exception on technical error.
   */
  public static synchronized void processPhoto(final FileHandler fileHandler, final Photo photo,
      final File image, final Watermark watermark) throws Exception {
    if (image != null) {
      try {
        photo.setFileName(StringUtil.normalize(image.getName()));
        final HandledFile handledImageFile = getHandledFile(fileHandler, photo);
        fileHandler.copyFile(image, handledImageFile);
        new PhotoProcess(handledImageFile, photo, watermark).process();
      } finally {
        FileUtils.deleteQuietly(image);
      }
    }
  }

  /**
   * Pastes media from a source to a destination.
   * @param fileHandler the file handler (space quota management).
   * @param fromPK the source.
   * @param media the destination.
   * @param cut true if it is a cut operation, false if it is a copy one.
   */
  public static synchronized void pasteInternalMedia(final FileHandler fileHandler,
      final MediaPK fromPK, final InternalMedia media, final boolean cut) {
    InternalMedia fromMedia = media.getType().newInstance();
    fromMedia.setMediaPK(fromPK);
    fromMedia.setFileName(media.getFileName());
    final HandledFile fromDir = getHandledFile(fileHandler, fromMedia).getParentHandledFile();
    final HandledFile toDir = getHandledFile(fileHandler, media).getParentHandledFile();

    // Copy and rename all media that exist into source folder
    if (fromDir.exists()) {

      // Copy all files which the name starts with the media identifier
      Collection<HandledFile> srcFiles =
          fromDir.listFiles(
              or(
                  prefixFileFilter(fromPK.getId()),
                  asFileFilter(file -> file.getName().matches(
                      "^" + VIDEO_THUMBNAIL_FILE_PREFIX + "[0-9]+" +
                          VIDEO_THUMBNAIL_FILE_EXTENSION + "$"))
              ),
              falseFileFilter());
      int substringIndex = fromPK.getId().length();
      for (HandledFile srcFile : srcFiles) {
        String srcFileName = srcFile.getFile().getName();
        String dstFileName = (srcFileName.startsWith(fromPK.getId())) ?
            (media.getId() + srcFileName.substring(substringIndex)) : srcFileName;
        pasteFile(srcFile, toDir.getHandledFile(dstFileName), cut);
      }

      // Copy original image
      pasteFile(fromDir.getHandledFile(media.getFileName()), toDir.getHandledFile(media.
          getFileName()), cut);

      // On cut operation, deleting the source repo
      if (cut && !fromPK.getInstanceId().equals(media.getInstanceId())) {
        try {
          fromDir.delete();
        } catch (Exception e) {
          SilverLogger.getLogger(MediaUtil.class).error(
              "Unable to delete source folder : folder path = " + fromDir.getFile().getPath(), e);
        }
      }
    }
  }

  /**
   * Sets metadata to given instance which represents a photo in memory.
   * @param fileHandler the file handler (quota space management).
   * @param photo the photo to set.
   * @throws IOException
   * @throws MediaMetadataException
   */
  public static void setMetaData(final FileHandler fileHandler, final Photo photo)
      throws MediaMetadataException {
    try {
      setMetaData(fileHandler, photo, MessageManager.getLanguage());
    } catch (IOException e) {
      throw new MediaMetadataException(e);
    }
  }

  /**
   * In charge of processing an internal media.
   * @param <M>
   */
  private abstract static class MediaProcess<M extends InternalMedia> {

    private final HandledFile handledFile;
    private final M media;
    private final Set<MediaMimeType> supportedMimeTypes;

    private MediaMimeType physicalFileMimeType = null;
    private org.silverpeas.core.io.media.MetaData physicalFileMetaData = null;

    private MediaProcess(final HandledFile handledFile, final M media) {
      this.handledFile = handledFile;
      this.media = media;
      this.supportedMimeTypes = MediaMimeType.getSupportedMimeTypes(media.getType());
    }

    /**
     * Processes the media files.
     * @throws Exception
     */
    public void process() throws Exception {
      setInternalMetadata();
      generateFiles();
    }

    /**
     * Generates specific media files.
     * @throws Exception
     */
    protected abstract void generateFiles();

    /**
     * Sets the internal metadata. If metadata
     * @return true if internal data have been set, false otherwise.
     * @throws GalleryRuntimeException if no supported mime type.
     */
    private void setInternalMetadata() throws Exception {
      File fileForData = getHandledFile().getFile();
      MediaMimeType mediaMimeType = getPhysicalFileMimeType();
      if (supportedMimeTypes.contains(mediaMimeType)) {
        getMedia().setFileName(fileForData.getName());
        getMedia().setFileMimeType(mediaMimeType);
        getMedia().setFileSize(fileForData.length());
        final MediaType mediaType = getMedia().getType();
        if (mediaType.isPhoto()) {
          getMedia().getPhoto().setDefinition(getPhysicalFileMetaData().getDefinition());
        } else if (mediaType.isVideo()) {
          getMedia().getVideo().setDefinition(getPhysicalFileMetaData().getDefinition());
        }
        if (getPhysicalFileMetaData().getDuration() != null) {
          if (mediaType.isVideo()) {
            getMedia().getVideo()
                .setDuration(getPhysicalFileMetaData().getDuration().getTimeAsLong());
          } else if (mediaType.isSound()) {
            getMedia().getSound()
                .setDuration(getPhysicalFileMetaData().getDuration().getTimeAsLong());
          }
        }
        if (StringUtil.isNotDefined(getMedia().getTitle()) &&
            isDefined(getPhysicalFileMetaData().getTitle())) {
          getMedia().setTitle(getPhysicalFileMetaData().getTitle());
        }
      } else {
        getMedia().setFileName(null);
        try {
          throw new GalleryRuntimeException(
              "Mime-Type of " + fileForData.getName() + " is not supported (" +
                  FileUtil.getMimeType(fileForData.getPath()) + ")");
        } finally {
          getHandledFile().delete();
        }
      }
    }

    /**
     * Gets the meta data of the physical file.
     * @return meta data.
     */
    org.silverpeas.core.io.media.MetaData getPhysicalFileMetaData() {
      if (physicalFileMetaData == null) {
        physicalFileMetaData = MetadataExtractor.get().extractMetadata(getHandledFile().getFile());
      }
      return physicalFileMetaData;
    }

    /**
     * Gets lazily the mime type from the physical file which represents the media file.
     * @return the mime type of the physical file.
     */
    private MediaMimeType getPhysicalFileMimeType() {
      if (physicalFileMimeType == null) {
        physicalFileMimeType = MediaMimeType.fromFile(getHandledFile().getFile());
      }
      return physicalFileMimeType;
    }

    /**
     * Gets the handled physical file.
     * @return
     */
    HandledFile getHandledFile() {
      return handledFile;
    }

    /**
     * Gets the representation of the handled media.
     * @return the media instance.
     */
    public M getMedia() {
      return media;
    }
  }

  private static class SoundProcess extends MediaProcess<Sound> {
    private SoundProcess(final HandledFile handledFile, final Sound media) {
      super(handledFile, media);
    }

    @Override
    protected void generateFiles() {
      // No generation.
    }
  }

  private static class VideoProcess extends MediaProcess<Video> {
    private VideoProcess(final HandledFile handledFile, final Video media) {
      super(handledFile, media);
    }

    @Override
    protected void generateFiles() {
      VideoThumbnailExtractor vte = VideoThumbnailExtractor.get();
      if (vte.isActivated()) {
        vte.generateThumbnailsFrom(getPhysicalFileMetaData(), super.getHandledFile().getFile());
      }
    }
  }

  private static class PhotoProcess extends MediaProcess<Photo> {
    private Set<AbstractImageToolOption> watermarkNormalOptions = Collections.emptySet();
    private Set<AbstractImageToolOption> watermarkThumbnailOptions = Collections.emptySet();
    private HandledFile[] thumbnailSrc = new HandledFile[2];

    private List<MetaData> cachedIptcMetadata = null;

    private PhotoProcess(final HandledFile handledFile, final Photo photo,
        final Watermark watermark) {
      super(handledFile, photo);
      computeWatermarks(watermark);
    }

    private static ImageTool getImageTool() {
      return ImageTool.get();
    }

    @Override
    protected void generateFiles() {

      final Photo photo = getMedia();
      if (photo.isPreviewable()) {

        // Registering the size of the image
        registerResolutionData();

        // Creating normal pictures (normal and watermark)
        createNormals(photo);

        // Creating preview and thumbnails
        try {
          createThumbnails();
        } catch (final Exception e) {
          SilverLogger.getLogger(MediaUtil.class)
              .error("image = " + photo.getTitle() + " (#" + photo.getId() + ")", e);
        }
      }
    }

    /**
     * Registers the resolution of a photo.
     */
    private void registerResolutionData() {
      if (getMedia().getDefinition().getWidth() != 0 &&
          getMedia().getDefinition().getHeight() != 0) {
        // definition already set.
        return;
      }
      String[] widthAndHeight = null;
      try {
        widthAndHeight = getImageTool().getImageInfo(super.getHandledFile().getFile(),
            WIDTH_IN_PIXEL, HEIGHT_IN_PIXEL);
      } catch (Exception e) {
        SilverLogger.getLogger(this)
            .error("impossible to read the width and height of file ''{0}''",
                new Object[]{super.getHandledFile().getFile().getName()}, e);
      }
      final int widthAndHeightSize = 2;
      if (widthAndHeight == null || widthAndHeight.length != widthAndHeightSize) {
        getMedia().setDefinition(Definition.fromZero());
      } else {
        getMedia().setDefinition(
            Definition.of(Integer.valueOf(widthAndHeight[0]), Integer.valueOf(widthAndHeight[1])));
      }
    }

    /**
     * Creates all the thumbnails around a photo.
     * @throws Exception on technical error.
     */
    private void createThumbnails() throws Exception {
      Photo photo = getMedia();

      // File name
      final String photoId = photo.getId();

      // Processing order :
      // Large (preview without watermark)
      // Preview
      // Medium
      // Small
      // Tiny
      final MediaResolution[] mediaResolutions =
          new MediaResolution[]{LARGE, PREVIEW, MEDIUM, SMALL, TINY};
      final HandledFile originalFile = super.getHandledFile();
      final HandledFile[] sources = Stream.of(thumbnailSrc).toArray(HandledFile[]::new);
      final String originalFileExt = "." + getExtension(photo.getFileName());
      for (MediaResolution mediaResolution : mediaResolutions) {
        final boolean watermarkApplicable = mediaResolution.isWatermarkApplicable();
        final int index = watermarkApplicable && thumbnailSrc[1] != null ? 1 : 0;
        final HandledFile currentThumbnail = originalFile.getParentHandledFile()
            .getHandledFile(photoId + mediaResolution.getThumbnailSuffix() + originalFileExt);
        generateThumbnail(thumbnailSrc[index], currentThumbnail, mediaResolution);
        // The first thumbnail that has to be created must be the larger one and without watermark.
        // This first thumbnail is cached and reused for the following thumbnail creation.
        if (sources[index] == thumbnailSrc[index]) {
          thumbnailSrc[index] = currentThumbnail;
        }
      }
      if (sources[1] != null) {
        sources[1].delete();
      }
    }

    /**
     * Return the written file
     * @param sourceFile
     * @param outputFile
     * @param mediaResolution
     * @throws Exception
     */
    private void generateThumbnail(final HandledFile sourceFile, final HandledFile outputFile,
        MediaResolution mediaResolution) throws Exception {
      final boolean watermarkToApply =
          mediaResolution.isWatermarkApplicable() && !watermarkThumbnailOptions.isEmpty();
      final Definition definition = getMedia().getDefinition();
      final boolean resizeToPerform = definition.getWidth() > mediaResolution.getWidth() ||
          definition.getHeight() > mediaResolution.getHeight();
      if (!resizeToPerform && !watermarkToApply) {
        // Simple copy
        sourceFile.copyFile(outputFile);
        return;
      }

      // Optimized media processing
      final Set<AbstractImageToolOption> options = new HashSet<>();
      options.add(OrientationOption.auto());
      if (resizeToPerform) {
        options.add(DimensionOption
            .widthAndHeight(mediaResolution.getWidth(), mediaResolution.getHeight()));
      }
      getImageTool().convert(sourceFile.getFile(), outputFile.getFile(), options, PREVIEW_WORK,
          GEOMETRY_SHRINK);
    }

    private void computeWatermarks(Watermark watermark) {
      String wText = null;
      File wImage = null;
      String thumbnailWText = null;
      File thumbnailWImage = null;
      final Photo photo = getMedia();
      if (watermark != null && watermark.isEnabled()) {
        try {
          wText = defaultStringIfNotDefined(
              getIptcWatermarkValue(watermark, photo, watermark.getIPTCPropertyForHD()),
              watermark.getTextForHD());
          wImage = watermark.getImageForHD();
          if (watermark.isDefinedForThumbnails()) {
            thumbnailWText = defaultStringIfNotDefined(
                getIptcWatermarkValue(watermark, photo, watermark.getIPTCPropertyForThumbnails()),
                watermark.getTextForThumbnails());
            thumbnailWImage = watermark.getImageForThumbnails();
          }
        } catch (MediaMetadataException e) {
          SilverLogger.getLogger(MediaUtil.class).silent(e).error(
              "Bad image file format " + super.getHandledFile().getFile().getPath() + ": " +
                  e.getMessage());
        } catch (IOException e) {
          SilverLogger.getLogger(MediaUtil.class).silent(e).error(
              "Bad metadata encoding in image " + super.getHandledFile().getFile().getPath() +
                  ": " + e.getMessage());
        }
      }
      watermarkNormalOptions = computeWatermarkOptions(wText, wImage);
      watermarkThumbnailOptions = computeWatermarkOptions(thumbnailWText, thumbnailWImage);
    }

    private void createNormals(final Photo photo) {
      final String originalFileExt = "." + getExtension(photo.getFileName());
      // Normal duplication (for orientation)
      final HandledFile normalFile = super.getHandledFile()
          .getParentHandledFile()
          .getHandledFile(photo.getId() + "_normal" + originalFileExt);
      getImageTool().convert(super.getHandledFile().getFile(), normalFile.getFile(),
          OrientationOption.auto());
      thumbnailSrc[0] = normalFile;
      // Photo duplication that is stamped with a Watermark.
      if (!watermarkNormalOptions.isEmpty()) {
        final HandledFile watermarkFile = super.getHandledFile()
            .getParentHandledFile()
            .getHandledFile(photo.getId() + "_watermark" + originalFileExt);
        getImageTool().convert(super.getHandledFile().getFile(), watermarkFile.getFile(),
            watermarkNormalOptions);
      }
      if (!watermarkThumbnailOptions.isEmpty()) {
        final HandledFile watermarkFileForThumbnails = super.getHandledFile().getParentHandledFile()
            .getHandledFile(photo.getId() + "_watermark_for_thumbnails" + originalFileExt);
        getImageTool()
            .convert(super.getHandledFile().getFile(), watermarkFileForThumbnails.getFile(),
                watermarkThumbnailOptions);
        thumbnailSrc[1] = watermarkFileForThumbnails;
      }
    }

    private Set<AbstractImageToolOption> computeWatermarkOptions(final String textToWatermark,
        final File imageToWatermark) {
      final Set<AbstractImageToolOption> options = new HashSet<>();
      if (isDefined(textToWatermark)) {
        final WatermarkTextOption wText = WatermarkTextOption.text(textToWatermark);
        if (imageToWatermark != null) {
          wText.withAnchoringPosition(AnchoringPosition.SOUTH_WEST);
        }
        options.add(wText);
      }
      if (imageToWatermark != null) {
        options.add(WatermarkImageOption.image(imageToWatermark));
      }
      if (!options.isEmpty()) {
        options.add(OrientationOption.auto());
      }
      return options;
    }

    /**
     * Gets lazily the IPTC data from a photo.
     * @return
     * @throws MediaMetadataException
     * @throws IOException
     */
    private List<MetaData> getIptcMetaData() throws MediaMetadataException, IOException {
      if (cachedIptcMetadata == null) {
        final MediaMetadataExtractor extractor =
            new DrewMediaMetadataExtractor(getMedia().getInstanceId());
        cachedIptcMetadata = extractor.extractImageIptcMetaData(super.getHandledFile().getFile());
      }
      return cachedIptcMetadata;
    }

    private String getIptcWatermarkValue(final Watermark watermark, final Photo photo,
        final String property)
        throws MediaMetadataException, IOException {
      String value = null;
      if (watermark.isBasedOnIPTC() && photo.getFileMimeType().isIPTCCompliant()) {
        value = getIptcMetaData().stream()
            .filter(i -> property.equalsIgnoreCase(i.getProperty()))
            .map(MetaData::getValue)
            .findFirst()
            .orElse(null);
      }
      return value;
    }
  }
}