|
@@ -0,0 +1,337 @@
|
|
|
|
|
+package cn.chinaunicom.omniFlowNetCompute.until;
|
|
|
|
|
+
|
|
|
|
|
+import com.drew.imaging.ImageMetadataReader;
|
|
|
|
|
+import com.drew.imaging.ImageProcessingException;
|
|
|
|
|
+import com.drew.metadata.Metadata;
|
|
|
|
|
+import com.drew.metadata.exif.ExifSubIFDDirectory;
|
|
|
|
|
+import com.drew.metadata.file.FileSystemDirectory;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Component;
|
|
|
|
|
+import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
+
|
|
|
|
|
+import javax.imageio.ImageIO;
|
|
|
|
|
+import javax.imageio.ImageReader;
|
|
|
|
|
+import javax.imageio.stream.ImageInputStream;
|
|
|
|
|
+import java.io.ByteArrayInputStream;
|
|
|
|
|
+import java.io.File;
|
|
|
|
|
+import java.io.IOException;
|
|
|
|
|
+import java.io.InputStream;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.time.ZoneId;
|
|
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Component
|
|
|
|
|
+public class ImageUtil {
|
|
|
|
|
+ // EXIF时间格式:通常为 "yyyy:MM:dd HH:mm:ss"
|
|
|
|
|
+ private static final DateTimeFormatter EXIF_DATE_FORMATTER =
|
|
|
|
|
+ DateTimeFormatter.ofPattern("yyyy:MM:dd HH:mm:ss");
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 验证图片时间(增强版,支持多种时间获取方式)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static Map<String, Object> validatePhotoTime(MultipartFile file, Date startTime) {
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ if (file == null || file.isEmpty()) {
|
|
|
|
|
+ result.put("valid", false);
|
|
|
|
|
+ result.put("message", "文件为空");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String originalFilename = file.getOriginalFilename();
|
|
|
|
|
+ String contentType = file.getContentType();
|
|
|
|
|
+
|
|
|
|
|
+ log.info("验证图片时间 - 文件名: {}, 文件类型: {}, 文件大小: {}字节",
|
|
|
|
|
+ originalFilename, contentType, file.getSize());
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 尝试多种方式获取图片时间
|
|
|
|
|
+ Date photoTime = null;
|
|
|
|
|
+ String timeSource = "";
|
|
|
|
|
+
|
|
|
|
|
+ // 方式1:尝试从EXIF信息获取
|
|
|
|
|
+ photoTime = getPhotoTimeFromExif(file);
|
|
|
|
|
+ if (photoTime != null) {
|
|
|
|
|
+ timeSource = "EXIF信息";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 方式2:如果EXIF失败,尝试使用文件最后修改时间
|
|
|
|
|
+ if (photoTime == null) {
|
|
|
|
|
+ photoTime = getPhotoTimeFromFileMetadata(file);
|
|
|
|
|
+ if (photoTime != null) {
|
|
|
|
|
+ timeSource = "文件元数据";
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 方式3:如果前两种方式都失败,使用当前时间作为最后手段
|
|
|
|
|
+ if (photoTime == null) {
|
|
|
|
|
+ photoTime = new Date();
|
|
|
|
|
+ timeSource = "当前系统时间";
|
|
|
|
|
+ log.warn("无法获取图片拍摄时间,使用当前系统时间作为替代");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.info("获取到图片时间: {} (来源: {})", photoTime, timeSource);
|
|
|
|
|
+
|
|
|
|
|
+ Date currentTime = new Date();
|
|
|
|
|
+
|
|
|
|
|
+ // 校验拍摄时间是否在巡检开始时间之后
|
|
|
|
|
+ if (photoTime.before(startTime)) {
|
|
|
|
|
+ result.put("valid", false);
|
|
|
|
|
+ result.put("message", "图片" + timeSource + "早于巡检开始时间");
|
|
|
|
|
+ result.put("photoTime", photoTime);
|
|
|
|
|
+ result.put("startTime", startTime);
|
|
|
|
|
+ result.put("timeSource", timeSource);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 校验拍摄时间是否在当前时间之后(不应该发生)
|
|
|
|
|
+ if (photoTime.after(currentTime)) {
|
|
|
|
|
+ result.put("valid", false);
|
|
|
|
|
+ result.put("message", "图片" + timeSource + "晚于当前时间,时间设置可能不正确");
|
|
|
|
|
+ result.put("photoTime", photoTime);
|
|
|
|
|
+ result.put("currentTime", currentTime);
|
|
|
|
|
+ result.put("timeSource", timeSource);
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.put("valid", true);
|
|
|
|
|
+ result.put("message", "时间校验通过(时间来源: " + timeSource + ")");
|
|
|
|
|
+ result.put("photoTime", photoTime);
|
|
|
|
|
+ result.put("timeSource", timeSource);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("图片时间校验失败", e);
|
|
|
|
|
+ result.put("valid", false);
|
|
|
|
|
+ result.put("message", "图片时间校验失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从EXIF信息获取图片拍摄时间
|
|
|
|
|
+ */
|
|
|
|
|
+ private static Date getPhotoTimeFromExif(MultipartFile file) {
|
|
|
|
|
+ try (InputStream inputStream = new ByteArrayInputStream(file.getBytes())) {
|
|
|
|
|
+ Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
|
|
|
|
|
+
|
|
|
|
|
+ // 尝试从多个位置获取时间信息
|
|
|
|
|
+ Date photoDate = null;
|
|
|
|
|
+
|
|
|
|
|
+ // 1. 尝试从ExifSubIFDDirectory获取
|
|
|
|
|
+ ExifSubIFDDirectory exifDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
|
|
|
|
+ if (exifDirectory != null) {
|
|
|
|
|
+ photoDate = exifDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
|
|
|
|
|
+ if (photoDate == null) {
|
|
|
|
|
+ photoDate = exifDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (photoDate == null) {
|
|
|
|
|
+ photoDate = exifDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME_DIGITIZED);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (photoDate != null) {
|
|
|
|
|
+ log.debug("从EXIF获取到拍摄时间: {}", photoDate);
|
|
|
|
|
+ // 将UTC时间转换为北京时间(+8小时)
|
|
|
|
|
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
|
|
|
|
+ calendar.setTime(photoDate);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置目标时区为北京时间
|
|
|
|
|
+ Calendar targetCalendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
|
|
|
|
|
+ targetCalendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
|
|
|
|
|
+ targetCalendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH));
|
|
|
|
|
+ targetCalendar.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH));
|
|
|
|
|
+ targetCalendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY));
|
|
|
|
|
+ targetCalendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE));
|
|
|
|
|
+ targetCalendar.set(Calendar.SECOND, calendar.get(Calendar.SECOND));
|
|
|
|
|
+ targetCalendar.set(Calendar.MILLISECOND, calendar.get(Calendar.MILLISECOND));
|
|
|
|
|
+ return targetCalendar.getTime();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 2. 尝试从FileSystemDirectory获取
|
|
|
|
|
+ FileSystemDirectory fileSystemDirectory = metadata.getFirstDirectoryOfType(FileSystemDirectory.class);
|
|
|
|
|
+ if (fileSystemDirectory != null) {
|
|
|
|
|
+ photoDate = fileSystemDirectory.getDate(FileSystemDirectory.TAG_FILE_MODIFIED_DATE);
|
|
|
|
|
+ if (photoDate != null) {
|
|
|
|
|
+ log.debug("从文件系统元数据获取到修改时间: {}", photoDate);
|
|
|
|
|
+ // 将UTC时间转换为北京时间(+8小时)
|
|
|
|
|
+ Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("UTC"));
|
|
|
|
|
+ calendar.setTime(photoDate);
|
|
|
|
|
+
|
|
|
|
|
+ // 设置目标时区为北京时间
|
|
|
|
|
+ Calendar targetCalendar = Calendar.getInstance(TimeZone.getTimeZone("Asia/Shanghai"));
|
|
|
|
|
+ targetCalendar.set(Calendar.YEAR, calendar.get(Calendar.YEAR));
|
|
|
|
|
+ targetCalendar.set(Calendar.MONTH, calendar.get(Calendar.MONTH));
|
|
|
|
|
+ targetCalendar.set(Calendar.DAY_OF_MONTH, calendar.get(Calendar.DAY_OF_MONTH));
|
|
|
|
|
+ targetCalendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY));
|
|
|
|
|
+ targetCalendar.set(Calendar.MINUTE, calendar.get(Calendar.MINUTE));
|
|
|
|
|
+ targetCalendar.set(Calendar.SECOND, calendar.get(Calendar.SECOND));
|
|
|
|
|
+ targetCalendar.set(Calendar.MILLISECOND, calendar.get(Calendar.MILLISECOND));
|
|
|
|
|
+ return targetCalendar.getTime();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ log.warn("图片不包含EXIF信息或无法读取时间信息");
|
|
|
|
|
+ return null;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (ImageProcessingException e) {
|
|
|
|
|
+ log.warn("图片EXIF处理异常: {}", e.getMessage());
|
|
|
|
|
+ return null;
|
|
|
|
|
+ } catch (IOException e) {
|
|
|
|
|
+ log.error("读取图片失败", e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("获取EXIF信息失败", e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 从文件元数据获取时间(备用方案)
|
|
|
|
|
+ */
|
|
|
|
|
+ private static Date getPhotoTimeFromFileMetadata(MultipartFile file) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ log.debug("无法从MultipartFile获取文件系统时间,使用上传时间");
|
|
|
|
|
+
|
|
|
|
|
+ // 返回当前时间作为备用
|
|
|
|
|
+ return new Date();
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("获取文件元数据失败", e);
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查图片是否包含EXIF信息
|
|
|
|
|
+ */
|
|
|
|
|
+ public static boolean hasExifInfo(MultipartFile file) {
|
|
|
|
|
+ try (InputStream inputStream = new ByteArrayInputStream(file.getBytes())) {
|
|
|
|
|
+ Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
|
|
|
|
|
+
|
|
|
|
|
+ // 检查是否有EXIF目录
|
|
|
|
|
+ ExifSubIFDDirectory exifDirectory = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
|
|
|
|
|
+ if (exifDirectory != null) {
|
|
|
|
|
+ // 尝试获取时间信息
|
|
|
|
|
+ Date photoDate = exifDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL);
|
|
|
|
|
+ if (photoDate == null) {
|
|
|
|
|
+ photoDate = exifDirectory.getDate(ExifSubIFDDirectory.TAG_DATETIME);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ boolean hasExif = photoDate != null;
|
|
|
|
|
+ log.debug("图片{}EXIF信息: {}", hasExif ? "包含" : "不包含", file.getOriginalFilename());
|
|
|
|
|
+ return hasExif;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return false;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.warn("检查EXIF信息失败: {}", e.getMessage());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 获取图片格式信息
|
|
|
|
|
+ */
|
|
|
|
|
+ public static Map<String, Object> getImageInfo(MultipartFile file) {
|
|
|
|
|
+ Map<String, Object> info = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ try (InputStream inputStream = new ByteArrayInputStream(file.getBytes())) {
|
|
|
|
|
+ // 获取图片格式
|
|
|
|
|
+ ImageInputStream imageInputStream = ImageIO.createImageInputStream(inputStream);
|
|
|
|
|
+ Iterator<ImageReader> readers = ImageIO.getImageReaders(imageInputStream);
|
|
|
|
|
+
|
|
|
|
|
+ if (readers.hasNext()) {
|
|
|
|
|
+ ImageReader reader = readers.next();
|
|
|
|
|
+ String formatName = reader.getFormatName();
|
|
|
|
|
+ info.put("format", formatName);
|
|
|
|
|
+
|
|
|
|
|
+ // 获取图片尺寸
|
|
|
|
|
+ reader.setInput(imageInputStream);
|
|
|
|
|
+ int width = reader.getWidth(0);
|
|
|
|
|
+ int height = reader.getHeight(0);
|
|
|
|
|
+ info.put("width", width);
|
|
|
|
|
+ info.put("height", height);
|
|
|
|
|
+
|
|
|
|
|
+ reader.dispose();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 检查EXIF
|
|
|
|
|
+ boolean hasExif = hasExifInfo(file);
|
|
|
|
|
+ info.put("hasExif", hasExif);
|
|
|
|
|
+
|
|
|
|
|
+ // 文件信息
|
|
|
|
|
+ info.put("fileName", file.getOriginalFilename());
|
|
|
|
|
+ info.put("fileSize", file.getSize());
|
|
|
|
|
+ info.put("contentType", file.getContentType());
|
|
|
|
|
+
|
|
|
|
|
+ log.info("图片信息: {}", info);
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("获取图片信息失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return info;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 简化的时间校验(用于测试)
|
|
|
|
|
+ */
|
|
|
|
|
+ public static Map<String, Object> validatePhotoTimeSimple(MultipartFile file, LocalDateTime startTime) {
|
|
|
|
|
+ Map<String, Object> result = new HashMap<>();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // 直接使用当前时间(跳过EXIF检查)
|
|
|
|
|
+ LocalDateTime photoTime = LocalDateTime.now();
|
|
|
|
|
+ LocalDateTime currentTime = LocalDateTime.now();
|
|
|
|
|
+
|
|
|
|
|
+ log.info("简化时间校验 - 使用当前时间: {}, 开始时间: {}", photoTime, startTime);
|
|
|
|
|
+
|
|
|
|
|
+ // 允许5分钟的误差(考虑到系统时间可能不同步)
|
|
|
|
|
+ LocalDateTime adjustedStartTime = startTime.minusMinutes(5);
|
|
|
|
|
+
|
|
|
|
|
+ if (photoTime.isBefore(adjustedStartTime)) {
|
|
|
|
|
+ result.put("valid", false);
|
|
|
|
|
+ result.put("message", "图片时间早于巡检开始时间(考虑5分钟误差)");
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ result.put("valid", true);
|
|
|
|
|
+ result.put("message", "时间校验通过(简化模式)");
|
|
|
|
|
+ result.put("photoTime", photoTime);
|
|
|
|
|
+ result.put("timeSource", "当前系统时间");
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("简化时间校验失败", e);
|
|
|
|
|
+ result.put("valid", false);
|
|
|
|
|
+ result.put("message", "时间校验失败: " + e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 调试方法:显示图片的所有EXIF信息
|
|
|
|
|
+ */
|
|
|
|
|
+ public static void debugExifInfo(MultipartFile file) {
|
|
|
|
|
+ try (InputStream inputStream = new ByteArrayInputStream(file.getBytes())) {
|
|
|
|
|
+ Metadata metadata = ImageMetadataReader.readMetadata(inputStream);
|
|
|
|
|
+
|
|
|
|
|
+ log.info("=== 图片EXIF信息调试 ===");
|
|
|
|
|
+ log.info("文件名: {}", file.getOriginalFilename());
|
|
|
|
|
+
|
|
|
|
|
+ metadata.getDirectories().forEach(directory -> {
|
|
|
|
|
+ log.info("目录: {}", directory.getName());
|
|
|
|
|
+ directory.getTags().forEach(tag -> {
|
|
|
|
|
+ log.info(" {}: {}", tag.getTagName(), tag.getDescription());
|
|
|
|
|
+ });
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("调试EXIF信息失败", e);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+}
|