2013年8月2日 星期五

追尋適合自己使用的圖形處理 API

A Simple C# Image Process API with Fluent Interface

哈囉大家,話說這一陣子寫程式數量實在太多,寫得很膩,所以跑來寫篇技術短文,稍微轉移一下注意力。(疑,會有幫助嗎?)

這次分享一下自製的圖形處理庫,實作成 Fluent API,本來自己的程式庫用的不是 Fluent 介面,而是一般的 static method(s),但是過去一段時間被一些圖片處理循序呼叫組合搞得很煩,想說既然這樣一不做二不休來個改版,而且這次還包含了解決自己長久以來的疑惑「控制 .NET 圖形處理後存檔 JPEG 的品質問題」,這次重寫也證明了我原來的想法是錯的,並非無解。

  1. 做成 Fluent API

    流利的 API? 哈哈,這名字聽起來就蠻爽的,做起來才會開心啊,如今我們早已習慣這種形式的 API,舉凡前端 jQuery、後端 LINQ、諸多 ORM framework 中都很容易見到這樣的設計,Dino 自己還蠻喜歡的,還是秉持著先構想用戶端使用介面的精神(用戶當然還是我自己 XD),習慣上還是先假想一下,API 具體來說我想要做成什麼樣子

    ImageProcess
    
      // 開啟圖片處理
      .Handle( Stream | Bitmap | FilePath )
    
      // 圖片處理   
      .Crop( X, Y, Width, Height )
      .Resize( Percentage, Side.Width | Side.Height )
      .Resize( Length, Side.Width | Side.Height )
      .Overlay( Bitmap | ImageProcess | FilePath, Placer )
      .Opacity( 0 ~ 1 ) 
    
      // 品質控制
      .Format( ImageFormat.Jpeg, ... )
      .Quality( 0 ~ 100 )
    
      // 記憶與還原點
      .Store( Name )
      .Fetch( Name )
    
      // 存檔
      .Save( Stream | FilePath )
    

    這是將第一階段我想做好的 API 全部列表,實際上還陸續增加了些。

    身為作者,當然是覺得介面應該還不會太囉唆啦,有建議的話在麻煩提供給我進行改善喔 XD

    下面這個例子,是一般情況下可能會用到的呼叫方式,這樣的程式碼就可以完成一次縮圖的處理。

    ImageProcess
      .Handle( Stream | Bitmap | FilePath )
      .Resize( Length, Side.Width | Side.Height )
      .Save( Stream | FilePath )
    
  2. 控制 .NET 圖形處理後存檔 JPEG 的品質問題

    過去不知道,誤認壓縮品質為這是無法控制的 .NET 程式庫結果,事實上,在網路上也大多是看到 InterpolationMode.HighQualityBicubic 設定的相關文章,更加惱怒的是,網路上充斥著抄襲的部落格文章,實在無恥!啊,講遠了,快回來啊,這點改善也可以肉眼就發現,卻是相當有限的。

    如果你的縮圖尺寸事實上不大,那麼差不多可以忽略品質問題,因為圖形小,視覺的問題不會那麼明顯,可是如果是大一點的圖,像是 Width 大於 400px,那麼 JPEG 的壓縮存檔可能會破壞掉圖像的品質,肉眼就看得很清楚,我過去就是不知道該怎麼調整 JPEG 存檔壓縮率,所以誤以為無法調整。

    所幸耐著性子找到 參考文章 終於知道調整的地方在哪

    (在這之前,我實在無法在 MSDN 上找到類似的例子,可能找文章的方向錯誤吧)

    public static void SaveAsQualityFactor(Bitmap bitmap, Stream stream, ImageFormat format = null, long? quality = null)
    {
        if (format == null)
        {
            format = ImageFormat.Jpeg;
        }
    
        if (quality.HasValue)
        {
            var codec = ImageCodecInfo
                            .GetImageEncoders()
                            .FirstOrDefault(x => x.FormatDescription.Equals(format.ToString(), StringComparison.CurrentCultureIgnoreCase));
    
            if (codec != null)
            {
                //Set the parameters for defining the quality of the thumbnail... here it is set to 100%
                var encoderParameters = new EncoderParameters(1);
    
                encoderParameters.Param[0] = new EncoderParameter(Encoder.Quality, quality.Value);
    
                bitmap.Save(stream, codec, encoderParameters);
            }
            else
            {
                bitmap.Save(stream, format);
            }
        }
        else
        {
            bitmap.Save(stream, format);
        }
    }
    

    那些關鍵程式行顯示出,藉由對於 codec 的細部參數控制,你可以設定出無損格式的 JPEG 存檔,當然檔案一定會隨之變大,所以我實務上僅在關鍵圖片上以幾乎無損方式存檔(理論上吧 XD),其他小縮圖則不考慮這樣處理

那麼就動手來做吧,咻一下程式就可以寫完了,程式碼有點長,直接由文末的附件下載就好了

本來想說這篇文簡單寫一下就好,沒想到還是多花了些時間,感覺有需要時候的再來探討程式碼,現在就先跳轉到用 LINQPad 來測試 ImageProcess 類別:

var stamp = ImageProcess
        // 讀檔
        .Handle( "浮水印圖檔.png" )
        // Debug 資訊
        .TakeBitmap( x => x.Dump() )
        // 縮小比例 12%
        .Resize( 0.12, Side.Width )
        // 透明度 80% 
        .Opacity( 0.8 )
        // Debug 資訊
        .TakeBitmap( x => x.Dump() );

ImageProcess
  // 讀檔
  .Handle( "原圖.jpg" )
  // Debug 資訊
  .Size( x => x.Dump() )
  // 圖寬縮為 461px, 圖高依計算比例縮小
  .Resize( 461, Side.Width )
  // Debug 資訊
  .Size( x => x.Dump() )
  // 灰階化
  .GrayScale()
  // 記憶起來
  .Store( "A" )
  // 四角落浮水印
  .Overlay( stamp, (s1, s2) => new Point(10, 10) )
  .Overlay( stamp, (s1, s2) => new Point(s1.Width - s2.Width - 10, s1.Height - s2.Height - 10) )
  .Overlay( stamp, (s1, s2) => new Point(s1.Width - s2.Width - 10, 10) )
  .Overlay( stamp, (s1, s2) => new Point(10, s1.Height - s2.Height - 10) )
  // 100% 無損格式 
  .Quality( 100 )
  // JPEG
  .Format( ImageFormat.Jpeg )
  // 存檔
  .Save( "合成輸出.jpg" )
  // PNG
  .Format( ImageFormat.Png )
  // 存檔
  .Save( "合成輸出.png" )
  // Debug 資訊
  .TakeBitmap( x => x.Dump() )
  // 提取記憶點
  .Fetch( "A" )
  // Debug 資訊
  .TakeBitmap( x => x.Dump() );
[Gist] ImageProcess.cs

沒有留言: