新增 EXIF 数据处理模型及相关函数
This commit is contained in:
1 parent
bae2721935
commit
e9e1c8318c
2 files changed
+277
-3
No files matched your search
@@ -0,0 +1,259 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
基础文件信息(文件级元数据)
|
||||||
|
这些信息即使图片没有 EXIF、XMP、IPTC 等元数据,也会存在,来自文件本身:
|
||||||
|
| 字段名 | 示例 | 说明 |
|
||||||
|
| ---------------------------------------------- | --------------------------- | --------------- |
|
||||||
|
| `FileName` | `image.jpg` | 文件名 |
|
||||||
|
| `FileSize` | `2.3 MB` | 文件大小 |
|
||||||
|
| `FileModifyDate` | `2024:11:04 10:40:29+08:00` | 最后修改时间(文件系统时间戳) |
|
||||||
|
| `FileCreateDate` | `2025:06:29 20:23:38+08:00` | 创建时间(视系统而定) |
|
||||||
|
| `FileAccessDate` | `2025:06:29 20:23:42+08:00` | 访问时间 |
|
||||||
|
| `Directory` | `D:/photos/` | 所在路径 |
|
||||||
|
| `MIMEType` | `image/jpeg` | 文件类型 |
|
||||||
|
| `FileType` / `FileTypeExtension` | `JPEG` / `JPG` | 扩展名和类型 |
|
||||||
|
| `ImageSize` | `5184x3888` | 像素尺寸 |
|
||||||
|
| `Megapixels` | `20.1` | 计算得出 |
|
||||||
|
| `EncodingProcess` | `Baseline DCT` | JPEG 编码方式 |
|
||||||
|
| `ColorComponents` | `3` | 通道数(RGB 通常为 3) |
|
||||||
|
| `YResolution`, `XResolution`, `ResolutionUnit` | `180`, `180`, `inch` | DPI 设置 |
|
||||||
|
*/
|
||||||
|
|
||||||
|
type BaseImageInfo struct {
|
||||||
|
FileName string `json:"FileName"`
|
||||||
|
FileSize int64 `json:"FileSize"`
|
||||||
|
ImageWidth int `json:"ImageWidth"`
|
||||||
|
ImageHeight int `json:"ImageHeight"`
|
||||||
|
ImageSize string `json:"ImageSize"`
|
||||||
|
// 文件类型
|
||||||
|
MIMEType string `json:"MIMEType"`
|
||||||
|
// 文件扩展名
|
||||||
|
FileType string `json:"FileType"`
|
||||||
|
// 文件扩展名
|
||||||
|
FileTypeExt string `json:"FileTypeExtension"`
|
||||||
|
// 文件最后修改时间
|
||||||
|
ModifyDate string `json:"FileModifyDate"`
|
||||||
|
// 文件创建时间
|
||||||
|
CreateDate string `json:"FileCreateDate"`
|
||||||
|
|
||||||
|
ColorSpace string `json:"ColorSpace"` // 色彩空间 (sRGB, Adobe RGB等)
|
||||||
|
BitsPerSample int `json:"BitsPerSample"` // 每个颜色通道的位深度
|
||||||
|
Resolution string `json:"Resolution"` // 分辨率信息
|
||||||
|
|
||||||
|
Quality int `json:"Quality"` // JPEG质量
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExifInfo struct {
|
||||||
|
Model string `json:"Model"`
|
||||||
|
Make string `json:"Make"`
|
||||||
|
ISO int `json:"ISO"`
|
||||||
|
GPSLatitude float64 `json:"GPSLatitude"`
|
||||||
|
GPSLongitude float64 `json:"GPSLongitude"`
|
||||||
|
ExposureTime float64 `json:"ExposureTime"`
|
||||||
|
Aperture float64 `json:"Aperture"`
|
||||||
|
FNumber float64 `json:"FNumber"` // 光圈值
|
||||||
|
FocalLength float64 `json:"FocalLength"`
|
||||||
|
LensID string `json:"LensID"`
|
||||||
|
Title string `json:"Title"`
|
||||||
|
Description string `json:"Description"`
|
||||||
|
DateTimeOrig string `json:"DateTimeOriginal"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ParsedExif struct {
|
||||||
|
BaseInfo BaseImageInfo
|
||||||
|
Exif ExifInfo
|
||||||
|
OtherFields map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 安全类型转换函数
|
||||||
|
func safeIntConvert(v interface{}) int {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int:
|
||||||
|
return val
|
||||||
|
case int64:
|
||||||
|
return int(val)
|
||||||
|
case float64:
|
||||||
|
return int(val)
|
||||||
|
case string:
|
||||||
|
if i, err := strconv.Atoi(val); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeInt64Convert(v interface{}) int64 {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(val)
|
||||||
|
case int64:
|
||||||
|
return val
|
||||||
|
case float64:
|
||||||
|
return int64(val)
|
||||||
|
case string:
|
||||||
|
if i, err := strconv.ParseInt(val, 10, 64); err == nil {
|
||||||
|
return i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeFloat64Convert(v interface{}) float64 {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return val
|
||||||
|
case float32:
|
||||||
|
return float64(val)
|
||||||
|
case int:
|
||||||
|
return float64(val)
|
||||||
|
case int64:
|
||||||
|
return float64(val)
|
||||||
|
case string:
|
||||||
|
if f, err := strconv.ParseFloat(val, 64); err == nil {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
func safeStringConvert(v interface{}) string {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case string:
|
||||||
|
return val
|
||||||
|
case int, int64, float64, float32:
|
||||||
|
return fmt.Sprintf("%v", val)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func SplitExifData(data map[string]interface{}) *ParsedExif {
|
||||||
|
var base BaseImageInfo
|
||||||
|
var exif ExifInfo
|
||||||
|
|
||||||
|
// 手动提取BaseImageInfo字段
|
||||||
|
if v, ok := data["FileName"]; ok {
|
||||||
|
base.FileName = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FileSize"]; ok {
|
||||||
|
base.FileSize = safeInt64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["ImageWidth"]; ok {
|
||||||
|
base.ImageWidth = safeIntConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["ImageHeight"]; ok {
|
||||||
|
base.ImageHeight = safeIntConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["ImageSize"]; ok {
|
||||||
|
base.ImageSize = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["MIMEType"]; ok {
|
||||||
|
base.MIMEType = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FileType"]; ok {
|
||||||
|
base.FileType = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FileTypeExtension"]; ok {
|
||||||
|
base.FileTypeExt = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FileModifyDate"]; ok {
|
||||||
|
base.ModifyDate = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FileCreateDate"]; ok {
|
||||||
|
base.CreateDate = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["ColorSpace"]; ok {
|
||||||
|
base.ColorSpace = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["BitsPerSample"]; ok {
|
||||||
|
base.BitsPerSample = safeIntConvert(v)
|
||||||
|
}
|
||||||
|
// 尝试从XResolution构建分辨率信息
|
||||||
|
if v, ok := data["XResolution"]; ok {
|
||||||
|
base.Resolution = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["JPEGQuality"]; ok {
|
||||||
|
base.Quality = safeIntConvert(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 手动提取ExifInfo字段
|
||||||
|
if v, ok := data["Model"]; ok {
|
||||||
|
exif.Model = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["Make"]; ok {
|
||||||
|
exif.Make = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["ISO"]; ok {
|
||||||
|
exif.ISO = safeIntConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["GPSLatitude"]; ok {
|
||||||
|
exif.GPSLatitude = safeFloat64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["GPSLongitude"]; ok {
|
||||||
|
exif.GPSLongitude = safeFloat64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["ExposureTime"]; ok {
|
||||||
|
exif.ExposureTime = safeFloat64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["Aperture"]; ok {
|
||||||
|
exif.Aperture = safeFloat64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FNumber"]; ok {
|
||||||
|
exif.FNumber = safeFloat64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["FocalLength"]; ok {
|
||||||
|
exif.FocalLength = safeFloat64Convert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["LensID"]; ok {
|
||||||
|
exif.LensID = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["Title"]; ok {
|
||||||
|
exif.Title = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["Description"]; ok {
|
||||||
|
exif.Description = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
if v, ok := data["DateTimeOriginal"]; ok {
|
||||||
|
exif.DateTimeOrig = safeStringConvert(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 定义已处理的字段
|
||||||
|
processedFields := map[string]bool{
|
||||||
|
"FileName": true, "FileSize": true, "ImageWidth": true, "ImageHeight": true,
|
||||||
|
"ImageSize": true, "MIMEType": true, "FileType": true, "FileTypeExtension": true,
|
||||||
|
"FileModifyDate": true, "FileCreateDate": true, "ColorSpace": true, "BitsPerSample": true,
|
||||||
|
"XResolution": true, "JPEGQuality": true,
|
||||||
|
"Model": true, "Make": true, "ISO": true, "GPSLatitude": true, "GPSLongitude": true,
|
||||||
|
"ExposureTime": true, "Aperture": true, "FNumber": true, "FocalLength": true,
|
||||||
|
"LensID": true, "Title": true, "Description": true, "DateTimeOriginal": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// 提取剩余字段
|
||||||
|
other := make(map[string]interface{})
|
||||||
|
for k, v := range data {
|
||||||
|
if !processedFields[k] {
|
||||||
|
other[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &ParsedExif{
|
||||||
|
BaseInfo: base,
|
||||||
|
Exif: exif,
|
||||||
|
OtherFields: other,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 辅助函数:判断字符串是否在切片中
|
||||||
|
func isIn(str string, slice ...string) bool {
|
||||||
|
for _, s := range slice {
|
||||||
|
if s == str {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"rear/internal/consts"
|
"rear/internal/consts"
|
||||||
|
"rear/internal/model"
|
||||||
"rear/internal/utils/tools"
|
"rear/internal/utils/tools"
|
||||||
"rear/pkg/logger"
|
"rear/pkg/logger"
|
||||||
"rear/pkg/utils"
|
"rear/pkg/utils"
|
||||||
@@ -149,9 +150,10 @@ func (pt *PictureTask) Run() {
|
|||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logger.Info("获取到 EXIF 数据", zap.Any("exifData", exifData))
|
|
||||||
|
|
||||||
return
|
// 分割 EXIF 数据
|
||||||
|
splitExifData := model.SplitExifData(exifData)
|
||||||
|
|
||||||
// 如果是非常规格式或 raw 则转换为 png ;如果是 PNG 则无损转换为 webp 或 jpg;如果是 webp 或 jpg 则进行压缩和其他处理
|
// 如果是非常规格式或 raw 则转换为 png ;如果是 PNG 则无损转换为 webp 或 jpg;如果是 webp 或 jpg 则进行压缩和其他处理
|
||||||
if fileType == string(consts.FormatJPG) {
|
if fileType == string(consts.FormatJPG) {
|
||||||
//options := tools.DefaultOptions()
|
//options := tools.DefaultOptions()
|
||||||
@@ -165,10 +167,23 @@ func (pt *PictureTask) Run() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 图像转换后,将转换后照片信息检索判断是否有必要进行压缩
|
||||||
|
|
||||||
|
// 压缩图像 【判断图像的大小是否需要压缩】
|
||||||
|
imgWidth := splitExifData.BaseInfo.ImageWidth
|
||||||
|
imgHeight := splitExifData.BaseInfo.ImageHeight
|
||||||
|
logger.Info("图像宽度",
|
||||||
|
zap.Int("width", imgWidth),
|
||||||
|
zap.Int("height", imgHeight),
|
||||||
|
)
|
||||||
|
|
||||||
|
// 如果图像的宽度或高度小于 800,则不进行压缩
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
// 分割 Hash 路径
|
// 分割 Hash 路径
|
||||||
//utils.HashUtils.HashThumbPath(config.CONFIG.AppDir, hash)
|
//utils.HashUtils.HashThumbPath(config.CONFIG.AppDir, hash)
|
||||||
|
|
||||||
// 压缩图像
|
|
||||||
// 获取 exif
|
// 获取 exif
|
||||||
// 保存到数据库
|
// 保存到数据库
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user