F#からFiddlerCoreを触る
リセマラ用にキャッシュプロキシでも作ってみようかと思ったんだ。
結局そこまで高速化はできなくて、そもそもリセマラ面倒になったよね。
SAZの読み書きを実装してみる
FiddlerではセッションをSAZという形式で保存することができます。FiddlerCoreでもこれを読み書きすることはできるのですが、Zipアーカイブの処理は自分で実装する必要があります。
例によってろくなドキュメントがないので、SampleAppのSAZ-DotNetZip.csを参考に実装していきます。その名の通りサンプルはDotNetZipを使っていますが、今回はSystem.IO.Compression.ZipFileを使います。
open System open Fiddler open System.IO open System.IO.Compression type SAZWriter(zipname) = let zip = ZipFile.Open(zipname, ZipArchiveMode.Create) interface ISAZWriter with member self.Filename = zipname member self.Comment with set(v) = () // これは必ず呼ばれるので例外を投げられない member self.EncryptionMethod = raise <| NotSupportedException() member self.EncryptionStrength = raise <| NotSupportedException() member self.SetPassword(password) = raise <| NotSupportedException() member self.AddFile(filename, writer) = use stream = zip.CreateEntry(filename).Open() writer.Invoke(stream) member self.CompleteArchive() = zip.Dispose() true type SAZReader(zipname) = let zip = ZipFile.OpenRead(zipname) interface ISAZReader with member self.Filename = zipname member self.Comment = raise <| NotSupportedException() member self.EncryptionMethod = raise <| NotSupportedException() member self.EncryptionStrength = raise <| NotSupportedException() member self.Close() = zip.Dispose() member self.GetRequestFileList() = seq { for x in zip.Entries -> x.FullName } |> Seq.filter (fun x -> x.StartsWith("raw/") && x.EndsWith("_c.txt")) |> Seq.toArray member self.GetFileStream(filename) = zip.GetEntry(filename).Open() member self.GetFileBytes(filename) = use src = zip.GetEntry(filename).Open() use buf = new MemoryStream() src.CopyTo(buf) buf.ToArray() type SAZProvider() = interface ISAZProvider with member self.BufferLocally = false member self.SupportsEncryption = false member self.CreateSAZ(zipname) = new SAZWriter(zipname) :> ISAZWriter member self.LoadSAZ(zipname) = new SAZReader(zipname) :> ISAZReader [<EntryPoint>] let main argv = FiddlerApplication.OnNotification.Add (fun e -> printfn "%s" e.NotifyString |> ignore) FiddlerApplication.oSAZProvider <- new SAZProvider() let sessions = Utilities.ReadSessionArchive(@"r:\archive.saz", true) printfn "%d sessions loaded" sessions.Length Utilities.WriteSessionArchive(@"r:\archive2.saz", sessions, null, true) 0
内包表記とパイプライン演算子GetRequestFileListで内包表記やパイプライン演算子を使っていますが、内包表記ひとつで済ますこともできます。
member self.GetRequestFileList() = [| for x in zip.Entries do let name = x.FullName if name.StartsWith("raw/") && name.EndsWith("_c.txt") then yield name |]
内包表記なしでも。
member self.GetRequestFileList() = zip.Entries |> Seq.map (fun x -> x.FullName) |> Seq.filter (fun x -> x.StartsWith("raw/") && x.EndsWith("_c.txt")) |> Seq.take 1 |> Seq.toArray
パイプライン使うのはLINQで見慣れてるし、内包表記もラムダの嵐を避けられて悪くないし・・・と思った結果がさっきのコードです。中途半端かもしれない。
もうひとつF#らしいところというと、GetFileBytesで使っているuse演算子でしょうか。C#でいうusingですが、インデントが深くならなくて素敵。
プロキシを立てる
SAZを読み込んでURLが一致するものにキャッシュから返そうかと思ったんですが、数が多いとSAZの読み書きが遅いとか、Fiddler使うの面倒とか・・・そんなわけでhttpで取得されるあらゆるファイルをURLに対応したパスにキャッシュするという雑な実装に。
open System open Fiddler open System.IO let port = 1601 let cachePathBase = @"R:\response\" let cachePathFor (session: Session) = let uri = new Uri(session.fullUrl) if uri.LocalPath.EndsWith("/") then None else Some(Path.Combine(cachePathBase, uri.Host, uri.LocalPath.Substring(1))) let onBeforeRequest (sess: Session) = match cachePathFor sess with | Some cachePath -> if (File.Exists(cachePath)) then sess.Ignore() sess.LoadResponseFromFile(cachePath) |> ignore printfn "hit: %s" sess.url |> ignore else sess.Tag <- cachePath printfn "save: %s" sess.url |> ignore | None -> sess.Ignore() let onBeforeResponse (sess: Session) = sess.SaveResponse(sess.Tag :?> string, false) [<EntryPoint>] let main argv = CONFIG.IgnoreServerCertErrors <- true FiddlerApplication.OnNotification.Add <| fun e -> Console.WriteLine e.NotifyString FiddlerApplication.Prefs.SetBoolPref("fiddler.network.streaming.abortifclientaborts", true) FiddlerApplication.add_BeforeRequest <| new SessionStateHandler(onBeforeRequest) FiddlerApplication.add_BeforeResponse <| new SessionStateHandler(onBeforeResponse) FiddlerApplication.Startup (port, FiddlerCoreStartupFlags.Default &&& ~~~FiddlerCoreStartupFlags.RegisterAsSystemProxy &&& ~~~FiddlerCoreStartupFlags.DecryptSSL) printfn "Port: %d\nPress Enter to exit." port stdin.ReadLine() |> ignore 0
FiddlerCoreではおなじみのBeforeRequestイベントですが、 BeforeRequest.Add (fun e -> ...)
とはできません。こんなエラーが出ます。
イベント
BeforeRequest
が標準以外の型です。このイベントが別の CLI 言語で宣言された場合、イベントにアクセスするには、このイベントに明示的な add_BeforeRequest メソッドや remove_BeforeRequest メソッドを使用する必要があります。このイベントが F# で宣言された場合、イベントの型をIDelegateEvent<_>
またはIEvent<_,_>
のインスタンス化にします。
どうやら EventHandler<T>
でないことが問題のようです。自分でSessionStateHandler型のインスタンスを作って、add_BeforeRequest
に渡さないといけません。
あとはoption使ってる以外はF#らしくないというか、すごく手続き型っぽいコードですねえ。。
Exifのサムネイルの縦横比を修正して、デジカメできちんと表示されるように
SDカードに過去の写真が結構入っている*1のですが、RAWで撮ってるので結構場所をとります。原本をPCに転送したらJPEGに置き換えたい。
と思って適当にJPEGに変換して入れてみると、個別に表示することはできても一覧でサムネイルが表示されない。
いろいろ探った結果、サムネイルの大きさがDCF規格の160x120*2と違う縦横比だったのが原因だったようです。ちなみに比率が正しければもっと大きくても表示できました*3。
プログラム書かない方へ:F6 Exifのインポート機能なども使えます(手間はかかりますが)。他にもサムネ再生成してくれるソフトはありそうですが、適当なものだと縦横比修正してくれないかも。
で、サムネイル再生成する何かをいつものようにC#で書こうとしたものの、System.DrawingやWPFでメタデータ付きのJPEGを書き出すとビッグエンディアンになってしまうようなのです。これではサムネイルだけでなく全く表示できなくなってしまいました。
そこで今回はExiv2のコマンドラインツールで埋め込むことにしました。exiv2 -it hoge.jpg
とすると、hoge-thumb.jpg を hoge.jpg に埋め込んでくれます。
またRAWからのJPEG化・サムネイル生成はPhotoshopをCOM経由で叩いてみました。
参考 Automating Photoshop With C# // Josh Wright
ただうちの環境ではタイプライブラリの場所が正しく登録されていなかったようで、手動で C:\Program Files (x86)\Adobe\Adobe Photoshop CS5\Plug-ins\Extensions\ScriptingSupport.8li
からtlbimpしました。
// using Photoshop; var src_dir = @"G:\Src\"; var target_dir = @"G:\Thumb\"; var exiv2_exe = @"R:\exiv2.exe"; var thumb_width = 320; var app = new Application(); //app.Visible = false; app.BackgroundColor = new SolidColor { RGB = new RGBColor { HexValue = "000000" } }; // 後々の座標・長さ指定はこの単位が適用される var ruler_old = app.Preferences.RulerUnits; app.Preferences.RulerUnits = PsUnits.psPixels; var export_opts = new ExportOptionsSaveForWeb { Format = PsSaveDocumentType.psJPEGSave, Blur = 0.2, Quality = 50, Optimized = true }; var save_opts = new JPEGSaveOptions { Quality = 8, FormatOptions = PsFormatOptionsType.psOptimizedBaseline }; foreach (var src in Directory.EnumerateFiles(src_dir, "*.cr2")) { var basename = Path.GetFileNameWithoutExtension(src); var img_path = $"{target_dir}{basename}.jpg"; var thumb_path = $"{target_dir}{basename}-thumb.jpg"; if (File.Exists(img_path)) File.Delete(img_path); if (File.Exists(thumb_path)) File.Delete(thumb_path); // サムネイル生成で使うExportはファイル名を指定できないので、後で移動する。 // ので、先にサムネ生成してから縮小前までUndoしてフルサイズ画像を書き出す。 var doc = app.Open(src); var state = doc.ActiveHistoryState; if (doc.Width > doc.Height) { doc.ResizeImage(Width: thumb_width, ResampleMethod: PsResampleMethod.psBicubicSharper); } else { // doc.ResizeImage(Height: thumb_width * 3 / 4, ResampleMethod: PsResampleMethod.psBicubicSharper); // サムネはこれでいいが、フルサイズ画像も(横長のまま)Exifで回転指定しないといけない・・・ throw new NotSupportedException("縦長画像は今後の課題。"); } doc.ResizeCanvas(thumb_width, thumb_width * 3 / 4, PsAnchorPosition.psMiddleCenter); doc.Export(target_dir, PsExportType.psSaveForWeb, export_opts); File.Move(img_path, thumb_path); doc.ActiveHistoryState = state; doc.SaveAs(img_path, save_opts); doc.Close(); // exiv2 -it foo.jpg // => foo.jpg に foo-thumb.jpg をサムネイル画像として埋め込む Process.Start(new ProcessStartInfo { FileName = exiv2_exe, Arguments = $"-it \"{img_path}\"", UseShellExecute = false, CreateNoWindow = true }).WaitForExit(); File.Delete(thumb_path); } app.Preferences.RulerUnits = ruler_old; //app.Quit();
ここまで書いてから、WICなんだからRAW形式もBitmapImageとかで読み込めて、Photoshopいらないのではと気付いた。面倒くささはいい勝負のような気がするけど…。