Remove exif in image file (#2168)

* Issue-1790[feature] add pre handler for attachment files

* Issue-2121 remove exif from JPEG

* Issue-2121 add an 'enable' option in preHandler

* Issue-2121 add an 'enable' option in preHandler

* Issue-2121 remove useless code

* Issue-2121 ImageFileMultipartFile

* Issue-2121 change method name

* Issue-1720 remove useless annotation and add author on file

* Issue-1720
1. Modify for code review.
2. Keep 'Orientation' Exif tag for images.

* Issue-1720 for review

* Issue-1720 for review

* Issue-1720 remove all Exif
pull/2185/head
ezio 2022-06-21 21:16:20 +08:00 committed by GitHub
parent eab2ee2903
commit f5d35dd26d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 226 additions and 1 deletions

View File

@ -75,6 +75,7 @@ ext {
jsoupVersion = '1.14.3'
embeddedRedisVersion = '0.6'
diffUtilsVersion = '4.11'
commonsImagingVersion = "1.0-alpha3"
}
dependencies {
@ -125,6 +126,7 @@ dependencies {
implementation "org.flywaydb:flyway-core:$flywayVersion"
implementation "com.google.zxing:core:$zxingVersion"
implementation "io.github.java-diff-utils:java-diff-utils:$diffUtilsVersion"
implementation "org.apache.commons:commons-imaging:$commonsImagingVersion"
implementation "org.iq80.leveldb:leveldb:$levelDbVersion"
runtimeOnly "com.h2database:h2:$h2Version"

View File

@ -1,8 +1,10 @@
package run.halo.app.handler.file;
import java.io.IOException;
import java.util.Collection;
import java.util.concurrent.ConcurrentHashMap;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.lang.NonNull;
import org.springframework.lang.Nullable;
@ -10,8 +12,11 @@ import org.springframework.stereotype.Component;
import org.springframework.util.Assert;
import org.springframework.util.CollectionUtils;
import org.springframework.web.multipart.MultipartFile;
import run.halo.app.exception.BadRequestException;
import run.halo.app.exception.FileOperationException;
import run.halo.app.exception.RepeatTypeException;
import run.halo.app.handler.prehandler.ByteMultipartFile;
import run.halo.app.handler.prehandler.FilePreHandlers;
import run.halo.app.model.entity.Attachment;
import run.halo.app.model.enums.AttachmentType;
import run.halo.app.model.support.UploadResult;
@ -26,6 +31,9 @@ import run.halo.app.model.support.UploadResult;
@Component
public class FileHandlers {
@Autowired
private FilePreHandlers filePreHandlers;
/**
* File handler container.
*/
@ -50,6 +58,13 @@ public class FileHandlers {
@NonNull
public UploadResult upload(@NonNull MultipartFile file,
@NonNull AttachmentType attachmentType) {
try {
byte[] bytes = filePreHandlers.process(file.getBytes());
file = new ByteMultipartFile(bytes, file.getOriginalFilename(), file.getName(),
file.getContentType());
} catch (IOException e) {
throw new BadRequestException("Get file bytes for preprocess failed", e);
}
return getSupportedType(attachmentType).upload(file);
}
@ -94,4 +109,5 @@ public class FileHandlers {
}
return handler;
}
}

View File

@ -0,0 +1,69 @@
package run.halo.app.handler.prehandler;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import org.jetbrains.annotations.NotNull;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.multipart.MultipartFile;
/**
* @author eziosudo
* @date 2022-06-19
*/
public class ByteMultipartFile implements MultipartFile {
private final byte[] imgContent;
private final String originalFilename;
private final String name;
private final String contentType;
public ByteMultipartFile(byte[] imgContent, String originalFilename, String name,
String contentType) {
this.imgContent = imgContent;
this.originalFilename = originalFilename;
this.name = name;
this.contentType = contentType;
}
@Override
public String getName() {
return this.name;
}
@Override
public String getOriginalFilename() {
return this.originalFilename;
}
@Override
public String getContentType() {
return this.contentType;
}
@Override
public boolean isEmpty() {
return imgContent == null || imgContent.length == 0;
}
@Override
public long getSize() {
return imgContent.length;
}
@Override
public byte[] getBytes() throws IOException {
return imgContent;
}
@Override
public InputStream getInputStream() throws IOException {
return new ByteArrayInputStream(imgContent);
}
@Override
public void transferTo(@NotNull File dest) throws IOException, IllegalStateException {
FileCopyUtils.copy(imgContent, dest);
}
}

View File

@ -0,0 +1,11 @@
package run.halo.app.handler.prehandler;
/**
* @author eziosudo
* @date 2022-06-16
*/
public interface FilePreHandler {
byte[] preProcess(byte[] bytes);
}

View File

@ -0,0 +1,37 @@
package run.halo.app.handler.prehandler;
import java.util.List;
import java.util.stream.Collectors;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
/**
* @author eziosudo
* @date 2022-06-16
*/
@Component
public class FilePreHandlers {
private final List<FilePreHandler> preHandlers;
FilePreHandlers(ApplicationContext applicationContext) {
preHandlers =
applicationContext.getBeanProvider(FilePreHandler.class)
.orderedStream()
.collect(Collectors.toList());
}
/**
*
*
* @param bytes
* @return
*/
public byte[] process(byte[] bytes) {
for (FilePreHandler filePreHandler : preHandlers) {
bytes = filePreHandler.preProcess(bytes);
}
return bytes;
}
}

View File

@ -0,0 +1,85 @@
package run.halo.app.handler.prehandler;
import java.io.BufferedOutputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.imaging.ImageReadException;
import org.apache.commons.imaging.ImageWriteException;
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.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import run.halo.app.model.properties.AttachmentProperties;
import run.halo.app.service.OptionService;
/**
* @author eziosudo
* @date 2022-06-16
*/
@Slf4j
@Component
@Order(value = 1)
public class PictureExifRemovalPreHandler implements FilePreHandler {
@Autowired
private OptionService optionService;
@Override
public byte[] preProcess(byte[] bytes) {
if (!isRemoveExifEnable()) {
return bytes;
}
try {
ImageMetadata metadata = Imaging.getMetadata(bytes);
final JpegImageMetadata jpegMetadata = (JpegImageMetadata) metadata;
if (null == jpegMetadata) {
return bytes;
}
final TiffImageMetadata exif = jpegMetadata.getExif();
if (null == exif) {
return bytes;
}
final TiffOutputSet outputSet = exif.getOutputSet();
if (null == outputSet) {
return bytes;
}
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
OutputStream os = new BufferedOutputStream(bos)) {
for (TiffOutputDirectory directory : outputSet.getDirectories()) {
for (TiffOutputField field : directory.getFields()) {
if (!StringUtils.equalsAnyIgnoreCase("Orientation", field.tagInfo.name)) {
outputSet.removeField(field.tagInfo);
}
}
}
new ExifRewriter().updateExifMetadataLossless(bytes, os, outputSet);
bytes = bos.toByteArray();
return bytes;
}
} catch (IllegalArgumentException e) {
log.info("Cannot parse to image format.");
} catch (ImageWriteException | ImageReadException | IOException e) {
log.info("Cannot get metadata from bytes.", e);
} catch (Exception e) {
log.info("Cannot check or remove Exif from bytes.", e);
}
return bytes;
}
private boolean isRemoveExifEnable() {
return optionService.getByPropertyOrDefault(
AttachmentProperties.REMOVE_IMAGE_EXIF_ENABLE,
Boolean.class, false);
}
}

View File

@ -15,6 +15,11 @@ public enum AttachmentProperties implements PropertyEnum {
*/
UPLOAD_IMAGE_PREVIEW_ENABLE("attachment_upload_image_preview_enable", Boolean.class, "true"),
/**
* Remove Exif in image when upload
*/
REMOVE_IMAGE_EXIF_ENABLE("attachment_EXIF_remove_enable", Boolean.class, "false"),
/**
* Upload max parallel uploads
*/