數碼相機拍攝的 JPG 照片在 **EXIF (Exchangeable Image File Format)** 元數據中記錄了拍攝時間、光圈、快門、ISO、GPS 等信息。但:
解決方案:**讀取 EXIF 拍攝時間 → 繪制到圖片右下角 → 保存新文件**,實現批量、可自定義字體/顏色/位置的日期水印。
.NET 中 `System.Drawing.Image` 封裝 GDI+,提供 `PropertyItems` 訪問 EXIF:
| Hex ID | 常量名 | 含義 | 類型 |
|---|---|---|---|
| 0x0132 | PropertyTagDateTime | 拍攝日期時間 | ASCII (2) |
| 0x010F | PropertyTagEquipMake | 設備製造商 | ASCII |
| 0x0110 | PropertyTagEquipModel | 設備型號 | ASCII |
| 0x829A | PropertyTagExifExposureTime | 曝光時間 | Rational (5) |
| 0x829D | PropertyTagExifFNumber | 光圈值 | Rational |
| 0x8827 | PropertyTagExifISOSpeed | ISO 速度 | Short (3) |
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 則為空數組
}
}
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 或無時間標籤
}
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"
}
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}");
}
}
}
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;
}
}
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);
}