首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> C# >> 關於C#

C# 讀取 EXIF 信息並為數碼照片批量添加拍攝日期水印

     編輯:關於C#

需求背景

數碼相機拍攝的 JPG 照片在 **EXIF (Exchangeable Image File Format)** 元數據中記錄了拍攝時間、光圈、快門、ISO、GPS 等信息。但:

  • 相機屏幕/打印照片通常**不直接顯示拍攝日期**。
  • 相機設置「日期水印」功能常缺失或不可自定義。
  • 圖片編輯軟件 (PS/BeautyCam) 批量添加水印繁瑣且易丟失 EXIF。

解決方案:**讀取 EXIF 拍攝時間 → 繪制到圖片右下角 → 保存新文件**,實現批量、可自定義字體/顏色/位置的日期水印。

核心技術:GDI+ 讀寫圖片元數據

.NET 中 `System.Drawing.Image` 封裝 GDI+,提供 `PropertyItems` 訪問 EXIF:

  • 每個 `PropertyItem`:`Id` (標識)、`Value` (字節數組)、`Len`、`Type` (數據類型)。
  • 關鍵 EXIF Tag:`0x0132` = `PropertyTagDateTime` (拍攝時間),類型 `PropertyTagTypeASCII` (ASCII 字符串)。

EXIF Tag 快速查閱表

Hex ID常量名含義類型
0x0132PropertyTagDateTime拍攝日期時間ASCII (2)
0x010FPropertyTagEquipMake設備製造商ASCII
0x0110PropertyTagEquipModel設備型號ASCII
0x829APropertyTagExifExposureTime曝光時間Rational (5)
0x829DPropertyTagExifFNumber光圈值Rational
0x8827PropertyTagExifISOSpeedISO 速度Short (3)

完整實現代碼

1. 獲取圖片所有 PropertyItem

public static PropertyItem[] GetExifProperties(string filePath) {
    using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read))
    using (var img = Image.FromStream(fs, true, false)) { // useEmbeddedColorManagement=true, validateImageData=false
        return img.PropertyItems; // 返回 PropertyItem[],若無 EXIF 則為空數組
    }
}

2. 提取拍攝時間字符串

private string GetDateTimeOriginal(PropertyItem[] props) {
    foreach (var prop in props) {
        if (prop.Id == 0x0132) { // PropertyTagDateTime
            // Value 為 ASCII 字節數組,以 \0 結尾
            return Encoding.ASCII.GetString(prop.Value).TrimEnd('\0');
        }
    }
    return "N/A"; // 無 EXIF 或無時間標籤
}

3. 格式化時間字符串

EXIF 時間格式:`"2006:06:07 16:33:41"` (冒號分隔日期)。需轉為標準格式:

private string FormatExifDateTime(string raw) {
    if (raw == "N/A") return raw;
    int spaceIdx = raw.IndexOf(' ');
    string datePart = raw.Substring(0, spaceIdx).Replace(':', '-'); // 2006-06-07
    string timePart = raw.Substring(spaceIdx + 1);
    return $"{datePart} {timePart}"; // "2006-06-07 16:33:41"
}

4. 繪制水印並保存

public void BatchStampDateTime(string[] imageFiles, string outputDir, 
    Font font, Color color, Point offsetFromBottomRight) {
    
    foreach (var file in imageFiles) {
        try {
            var props = GetExifProperties(file);
            string rawDt = GetDateTimeOriginal(props);
            if (rawDt == "N/A") continue; // 跳過無時間圖片
            
            string stampText = FormatExifDateTime(rawDt);
            
            using (var bmp = new Bitmap(file))
            using (var g = Graphics.FromImage(bmp)) {
                g.SmoothingMode = SmoothingMode.AntiAlias;
                g.TextRenderingHint = TextRenderingHint.ClearTypeGridFit;
                
                // 計算右下角位置 (預留 offset)
                SizeF textSize = g.MeasureString(stampText, font);
                float x = bmp.Width - textSize.Width - offsetFromBottomRight.X;
                float y = bmp.Height - textSize.Height - offsetFromBottomRight.Y;
                
                // 可選:半透明背景增強可讀性
                using (var bgBrush = new SolidBrush(Color.FromArgb(128, 0, 0, 0)))
                    g.FillRectangle(bgBrush, x - 2, y - 2, textSize.Width + 4, textSize.Height + 4);
                
                g.DrawString(stampText, font, new SolidBrush(color), x, y);
                
                // 保存 (保持原格式,可指定 EncoderParameters 控制 JPEG 質量)
                string outPath = Path.Combine(outputDir, Path.GetFileName(file));
                bmp.Save(outPath, bmp.RawFormat);
            }
        }
        catch (Exception ex) {
            Console.WriteLine($"處理失敗 {file}: {ex.Message}");
        }
    }
}

WinForms 界面整合示例

public partial class MainForm : Form {
    private List selectedFiles = new List();
    private string outputDir = "";
    
    // UI: ListBox 顯示文件, TextBox 顯示輸出目錄, Button 選擇/開始/清空
    
    private void btnAddFiles_Click(object sender, EventArgs e) {
        using (var ofd = new OpenFileDialog { 
            Filter = "JPG 圖片|*.jpg;*.jpeg", Multiselect = true }) {
            if (ofd.ShowDialog() == DialogResult.OK)
                selectedFiles.AddRange(ofd.FileNames);
            listBoxFiles.DataSource = null;
            listBoxFiles.DataSource = selectedFiles;
        }
    }
    
    private void btnSelectOutput_Click(object sender, EventArgs e) {
        using (var fbd = new FolderBrowserDialog()) {
            if (fbd.ShowDialog() == DialogResult.OK) {
                outputDir = fbd.SelectedPath;
                txtOutputDir.Text = outputDir;
            }
        }
    }
    
    private async void btnStart_Click(object sender, EventArgs e) {
        if (selectedFiles.Count == 0 || string.IsNullOrEmpty(outputDir)) return;
        
        btnStart.Enabled = false;
        var font = new Font("微軟雅黑", 16, FontStyle.Bold);
        var color = Color.Yellow;
        var offset = new Point(20, 20); // 距右下角 20px
        
        await Task.Run(() => BatchStampDateTime(
            selectedFiles.ToArray(), outputDir, font, color, offset));
        
        MessageBox.Show("批量添加完成!");
        btnStart.Enabled = true;
    }
}

關鍵注意事項與避坑指南

  • ⚠️ **`Image.FromFile` 會鎖定文件**:必須用 `Image.FromStream` + `FileStream` (關閉流後釋放鎖定),或 `File.ReadAllBytes` → `MemoryStream`。
  • ⚠️ **繪制後 EXIF 部分丟失**:GDI+ 保存時**不自動複製所有 PropertyItem**。關鍵信息 (時間、型號) 通常保留,但 GPS、MakerNote 等可能丟失。
    → 如需完整保留:讀取原 `PropertyItems` → 繪制後 `bmp.SetPropertyItem(prop)` 逐個寫回 (需過濾只讀標籤)。
  • ✅ **字體大小自適應**:根據圖片寬度動態計算 `Font.Size = bmp.Width / 50f` 等。
  • ✅ **高質量保存**:JPEG 保存時指定 `EncoderParameter(Encoder.Quality, 95L)` 避免二次壓縮失真。
  • ✅ **異步處理**:大批量圖片耗時長,必須 `Task.Run` 後台執行 + `IProgress` 回報進度,避免 UI 凍結。
  • 🔧 **現代替代**:.NET 6+ 可用 ImageSharp (純托管、跨平台、無 GDI+ 依賴) 替代 `System.Drawing`。

JPEG 質量控制保存示例

private void SaveJpegWithQuality(Image img, string path, long quality = 95) {
    var encoder = ImageCodecInfo.GetImageEncoders()
        .First(c => c.FormatID == ImageFormat.Jpeg.Guid);
    var params = new EncoderParameters(1);
    params.Param[0] = new EncoderParameter(Encoder.Quality, quality);
    img.Save(path, encoder, params);
}
  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved