/* * Copyright (C) 2008-2020, Juick * * 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. * * 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 . */ package com.juick.service; import java.awt.image.BufferedImage; import java.io.File; import java.io.IOException; import java.nio.file.CopyOption; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.util.Iterator; import javax.imageio.ImageIO; import javax.imageio.ImageReader; import javax.imageio.stream.ImageInputStream; import com.juick.model.Attachment; import com.juick.model.Message; import com.juick.model.Photo; import com.juick.model.User; import org.apache.commons.imaging.ImageReadException; import org.apache.commons.imaging.Imaging; import org.apache.commons.imaging.common.ImageMetadata; import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata; import org.apache.commons.imaging.formats.tiff.TiffField; import org.apache.commons.imaging.formats.tiff.constants.TiffTagConstants; import org.apache.commons.io.FileUtils; import org.apache.commons.io.FilenameUtils; import org.imgscalr.Scalr; import org.imgscalr.Scalr.Rotation; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.util.StringUtils; public class FileSystemStorageService implements StorageService { private static final Logger logger = LoggerFactory.getLogger(StorageService.class); private final String baseDir; private final String imgDir; private final String tmpDir; private final String avatarDir, avatarSmallDir, avatarOriginalDir; private final String fullImageDir, mediumImageDir, smallImageDir, thumbnailImageDir; public FileSystemStorageService(String baseDir, String tmpDir) { this.baseDir = baseDir; this.imgDir = Paths.get(baseDir, "i").toString(); this.tmpDir = tmpDir; this.avatarDir = Paths.get(imgDir, "a").toString(); this.avatarOriginalDir = Paths.get(imgDir, "ao").toString(); this.avatarSmallDir = Paths.get(imgDir, "as").toString(); this.fullImageDir = Paths.get(imgDir, "p").toString(); this.mediumImageDir = Paths.get(imgDir, "photos-1024").toString(); this.smallImageDir = Paths.get(imgDir, "photos-512").toString(); this.thumbnailImageDir = Paths.get(imgDir, "ps").toString(); } @Override public void setAttachmentMetadata(String baseUrl, Message msg) throws Exception { if (!StringUtils.isEmpty(msg.getAttachmentType())) { Photo photo = new Photo(); if (msg.getRid() > 0) { photo.setSmall(String.format("%sphotos-512/%d-%d.%s", baseUrl, msg.getMid(), msg.getRid(), msg.getAttachmentType())); photo.setMedium(String.format("%sphotos-1024/%d-%d.%s", baseUrl, msg.getMid(), msg.getRid(), msg.getAttachmentType())); photo.setThumbnail( String.format("%sps/%d-%d.%s", baseUrl, msg.getMid(), msg.getRid(), msg.getAttachmentType())); } else { photo.setSmall(String.format("%sphotos-512/%d.%s", baseUrl, msg.getMid(), msg.getAttachmentType())); photo.setMedium(String.format("%sphotos-1024/%d.%s", baseUrl, msg.getMid(), msg.getAttachmentType())); photo.setThumbnail(String.format("%sps/%d.%s", baseUrl, msg.getMid(), msg.getAttachmentType())); } msg.setPhoto(photo); String imageName = String.format("%s.%s", msg.getMid(), msg.getAttachmentType()); if (msg.getRid() > 0) { imageName = String.format("%s-%s.%s", msg.getMid(), msg.getRid(), msg.getAttachmentType()); } File fullImage = Paths.get(fullImageDir, imageName).toFile(); File mediumImage = Paths.get(mediumImageDir, imageName).toFile(); File smallImage = Paths.get(smallImageDir, imageName).toFile(); File thumbnailImage = Paths.get(thumbnailImageDir, imageName).toFile(); StringBuilder builder = new StringBuilder(); builder.append(baseUrl); builder.append(msg.getAttachmentType().equals("mp4") ? "video" : "p"); builder.append("/").append(msg.getMid()); if (msg.getRid() > 0) { builder.append("-").append(msg.getRid()); } builder.append(".").append(msg.getAttachmentType()); String originalUrl = builder.toString(); Attachment original = getAttachment(fullImage); original.setUrl(originalUrl); Attachment medium = getAttachment(mediumImage); medium.setUrl(photo.getMedium()); original.setMedium(medium); Attachment small = getAttachment(smallImage); small.setUrl(photo.getSmall()); original.setSmall(small); Attachment thumb = getAttachment(thumbnailImage); thumb.setUrl(photo.getMedium()); original.setThumbnail(thumb); msg.setAttachment(original); } } /** * Returns BufferedImage, same as ImageIO.read() does. * *

* JPEG images with EXIF metadata are rotated according to Orientation tag. * * @param imageFile a File to read from. */ private static BufferedImage readImageWithOrientation(File imageFile) throws IOException { BufferedImage image = ImageIO.read(imageFile); if (!FilenameUtils.getExtension(imageFile.getName()).equals("jpg")) { return image; } try { ImageMetadata metadata = Imaging.getMetadata(imageFile); if (metadata instanceof JpegImageMetadata jpegMetadata) { TiffField orientationField = jpegMetadata.findEXIFValue(TiffTagConstants.TIFF_TAG_ORIENTATION); if (orientationField != null) { int orientation = orientationField.getIntValue(); switch (orientation) { case TiffTagConstants.ORIENTATION_VALUE_ROTATE_90_CW: image = Scalr.rotate(image, Rotation.CW_90); break; case TiffTagConstants.ORIENTATION_VALUE_ROTATE_180: image = Scalr.rotate(image, Rotation.CW_180); break; case TiffTagConstants.ORIENTATION_VALUE_ROTATE_270_CW: image = Scalr.rotate(image, Rotation.CW_270); break; case TiffTagConstants.ORIENTATION_VALUE_MIRROR_HORIZONTAL: image = Scalr.rotate(image, Rotation.FLIP_HORZ); break; case TiffTagConstants.ORIENTATION_VALUE_MIRROR_VERTICAL: image = Scalr.rotate(image, Rotation.FLIP_VERT); break; case TiffTagConstants.ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_90_CW: image = Scalr.rotate(Scalr.rotate(image, Rotation.FLIP_HORZ), Rotation.CW_90); break; case TiffTagConstants.ORIENTATION_VALUE_MIRROR_HORIZONTAL_AND_ROTATE_270_CW: image = Scalr.rotate(Scalr.rotate(image, Rotation.FLIP_HORZ), Rotation.CW_270); break; case TiffTagConstants.ORIENTATION_VALUE_HORIZONTAL_NORMAL: default: // do nothing break; } } } } catch (ImageReadException | IOException e) { // failed to read metadata. // nothing to do here, return image as is. } return image; } @Override public void saveImageWithPreviews(String tempFilename, String outputFilename) throws IOException { String ext = FilenameUtils.getExtension(outputFilename); Path outputImagePath = Paths.get(fullImageDir, outputFilename); // this throws strange exceptions // Files.move(Paths.get(tmpDir, tempFilename), outputImagePath); FileUtils.moveFile(Paths.get(tmpDir, tempFilename).toFile(), outputImagePath.toFile()); BufferedImage originalImage = readImageWithOrientation(outputImagePath.toFile()); int width = originalImage.getWidth(); int height = originalImage.getHeight(); int maxDimension = Math.max(width, height); BufferedImage image1024 = (maxDimension > 1024) ? Scalr.resize(originalImage, 1024) : originalImage; BufferedImage image0512 = (maxDimension > 512) ? Scalr.resize(originalImage, 512) : originalImage; BufferedImage image0160 = (maxDimension > 160) ? Scalr.resize(originalImage, 160) : originalImage; ImageIO.write(image1024, ext, Paths.get(mediumImageDir, outputFilename).toFile()); ImageIO.write(image0512, ext, Paths.get(smallImageDir, outputFilename).toFile()); ImageIO.write(image0160, ext, Paths.get(thumbnailImageDir, outputFilename).toFile()); } private String getAvatarFileName(User user, String targetExtension) { return String.format("%s.%s", user.getUid(), targetExtension); } private Path getAvatarPath(User user) { return Paths.get(avatarDir, getAvatarFileName(user, "png")); } public void saveAvatar(String tempFilename, User user) throws IOException { String ext = FilenameUtils.getExtension(tempFilename); String originalName = getAvatarFileName(user, ext); Path originalPath = Paths.get(avatarOriginalDir, originalName); Path tmpPath = Paths.get(tmpDir, tempFilename); CopyOption[] copyOptions = new CopyOption[] { StandardCopyOption.REPLACE_EXISTING }; Files.move(tmpPath, originalPath,copyOptions); BufferedImage originalImage = ImageIO.read(originalPath.toFile()); String targetExt = "png"; if (ImageIO.write(Scalr.resize(originalImage, 96), targetExt, tmpPath.toFile())) { Files.move(tmpPath, getAvatarPath(user), copyOptions); } if (ImageIO.write(Scalr.resize(originalImage, 32), targetExt, tmpPath.toFile())) { Files.move(tmpPath, Paths.get(avatarSmallDir, getAvatarFileName(user, targetExt)), copyOptions); } } public Attachment getAttachment(File imgFile) throws IOException { Attachment attachment = new Attachment(); if (imgFile.exists()) { try (ImageInputStream stream = ImageIO.createImageInputStream(imgFile)) { Iterator iter = ImageIO.getImageReaders(stream); while (iter.hasNext()) { ImageReader reader = iter.next(); try { reader.setInput(stream); attachment.setWidth(reader.getWidth(reader.getMinIndex())); attachment.setHeight(reader.getHeight(reader.getMinIndex())); return attachment; } catch (Exception e) { logger.debug("Error reading {}, trying next reader", imgFile.getAbsolutePath()); } finally { reader.dispose(); } } } } return attachment; } public Attachment getAvatarMetadata(User user) throws IOException { return getAttachment(getAvatarPath(user).toFile()); } @Override public String getBaseDirectory() { return baseDir; } @Override public String getImageDirectory() { return imgDir; } @Override public String getTemporaryDirectory() { return tmpDir; } }