我想為放在網路圖床的照片建立預覽圖庫,需要輸入照片連結時,方便看縮圖挑照片得到連結網址。如果要自己寫程式來做,一般人直覺會建個資料表或 JSON 檔,關聯照片縮圖檔名與來源網址,再寫個照片瀏覽介面,點選照片縮圖時帶出原圖連結。

但,我有個大膽的想法 - 不要資料庫也不要搞 JSON,不用寫照片瀏覽介面,把原始連結藏在預覽 JPEG 檔的 EXIF 資訊,如此可直接用檔案總管瀏覽、管理,看到喜歡的照片,按右鍵從檔案內容/詳細資料複製來源網址,這樣才超符合 KISS (Keep It Simple, Stupid) 美學!

先看成果,我將照片轉成 640x640 以下的縮圖 JPG,用檔案總管或任何看圖軟體可直接瀏覽,按右鍵開啟內容視窗,來源網址就在詳細資料的作者欄。

要完成這項工作,我需要一個批次作業程式,讀入 URL 清單,決定唯一檔名,下載照片轉縮圖,將來源網址存入 EXIF 標籤,搞定收工。程式不難,只有幾項技術細節值得一值:

  1. 來源網址的檔名部分有可能重複,但縮圖檔名必須唯一。我的解法是用來源網址產生 MD5 雜湊碼,取前六碼加檔名組成唯一檔名,用雜湊而非亂數或時間標籤的理由是同一 URL 產生的檔名永遠相同,未來能以此判斷縮圖是否已存在。
  2. 原本還煩惱要怎麼產生縮圖,後來發現 .NET 已內建 System.Drawing.Image.GetThumbnailImage(),省下自幹或找元件的工夫。依原比例決定縮圖寬高的部分得自己算,但邏輯不難,加幾行就可搞定。
  3. JPEG 格式可加入標題、相機型號、光圈快門、作者等額外資料,術語叫 EXIF,NuGet 上有現成 ExifLibNet 套件,ExifLibrary.ImageFile.FromFile("...") 讀取檔案,.Properties.Set(ExifLibrary.ExifTag.Artist, "...") 設定,.Save("...") 儲存,前人種樹我乘涼,偉哉開源社群。

決定好做法,用 C# 或 PowerShell 實現只在彈指之間。我這裡用 PowerShell 示範:

Add-Type -Path .\ExifLibrary.dll
Add-Type -AssemblyName System.Drawing
$ErrorActionPreference = 'STOP'
function GetMD5Hash([string]$str) {
    # 輸入任意字串產生 MD5 雜湊
    $stringAsStream = [System.IO.MemoryStream]::new()
    $writer = [System.IO.StreamWriter]::new($stringAsStream)
    $writer.write($str)
    $writer.Flush()
    $stringAsStream.Position = 0
    Get-FileHash -InputStream $stringAsStream -Algorithm MD5 | Select-Object -ExpandProperty Hash
    $writer.Dispose()
    $stringAsStream.Dispose()
}

function CreateThumbnail([string]$srcPath, [string]$thumbnailPath, [int]$thumbnailSize, [string]$author) {
    [System.Drawing.Image]$srcImg = [System.Drawing.Image]::FromFile($srcPath)
    # 以縮圖尺寸上限計算縮放比例
    $ratio = 1
    if ($srcImg.Width -gt $srcImg.Height) {
        if ($srcImg.Width -gt $thumbnailSize) {
            $ratio = [float]$thumbnailSize / $srcImg.Width
        }
        elseif ($srcImg.Height -gt $thumbnailSize) {
            $ratio = [float]$thumbnailSize / $srcImg.Height
        }
    }
    # 計算縮圖寬高
    $thumbWidth = [System.Convert]::ToInt32($srcImg.Width * $ratio)
    $thumbHeight = [System.Convert]::ToInt32($srcImg.Height * $ratio)
    [System.Drawing.Image]$thumbnail = $srcImg.GetThumbnailImage($thumbWidth, $thumbHeight,{ param() $false }, [Intptr]::Zero)
    $thumbnail.Save($thumbnailPath, [System.Drawing.Imaging.ImageFormat]::Jpeg);
    $srcImg.Dispose()
    $thumbnail.Dispose()
    # 呼叫 ExifLibNet 元件在 EXIF 標籤寫入作者資訊
    [ExifLibrary.ImageFile]$file = [ExifLibrary.ImageFile]::FromFile($thumbnailPath)
    $file.Properties.Set([ExifLibrary.ExifTag]::Artist, $author)
    $file.Save($thumbnailPath)
}

$resFolder = "$PSScriptRoot\Thumbnails"
[System.IO.Directory]::CreateDirectory($resFolder) | Out-Null

Get-Content urls.txt | ForEach-Object {
    $url = $_
    $imgFileName = [System.IO.Path]::GetFileName($url.Split('?')[0].Split('#')[0])
    $hash = (GetMD5Hash $url).Substring(0, 6)
    $fullFileName = $hash  + "-" + $imgFileName
    $srcImgPath = Join-Path $resFolder $fullFileName
    Invoke-WebRequest -Uri $url -OutFile $srcImgPath
    $thumbnailPath = Join-Path $resFolder ($hash + "-" + [System.IO.Path]::GetFileNameWithoutExtension($imgFileName) + ".thumbnail.jpg")
    CreateThumbnail $srcImgPath $thumbnailPath 640 $url
    Remove-item $srcImgPath
    Move-Item $thumbnailPath $thumbnailPath.Replace('.thumbnail.jpg', '.jpg') -Force
}

想出內嵌 EXIF 這招,不需額外資料庫或資料檔,也省去寫瀏覽介面,不愧是 KISS 哲學的充分實踐,又一件得意作品,哈!

Example of downloading photos, converting thumbnail and setting EXIF by C#/PowerShell.


Comments

Be the first to post a comment

Post a comment