d.sunnyone.org
sunnyone.org

ページ

2015-08-23

PdfSharpでPDFファイルからJPEG画像を抽出する

.NET用のPDF操作ライブラリPdfSharpを使って、PDFファイルに含まれるJPEG画像のバイト列を抽出する。iTextではない。

「PdfSharp」NuGetパッケージをインストールして、以下の感じで一応OK。



StreamはStreamって言ってるけどSystem.IO.Streamではないのでバイト列をファイルごとに返すことにした。

参考:PDFsharp Sample: Export Images

2015-08-10

PowerShell と ImageMagick (Magick.NET)で画像を加工する

WindowsでImageMagickを使おうとすると、cmdの引数の扱いがだるいと思っていたのだけど、ImageMagickライブラリの.NETラッパーであるところのMagick.NETをPowerShellから呼んでしまえば、便利に呼べることに気づいた。convertコマンドのパラメータの順序とか考えなくていいので、正直convert使うより楽かもしれない。

基本的にMagick.NETを呼んでいけばOK(気づいたPowerShell固有の罠は以下に書いておく)。本格的に処理するのであればC#でコード書いたほうがいいと思うけど、そこまででもないワンタイムバッチ処理にどうぞ。ただし「Verb-Noun -Arg abc」styleでない、.NETクラス直呼び出しの「なんちゃってPowerShell」なので、がっかりされる前に宣言しておく。コンソールで打って問題ない量だと思うけど。

なお動かしてる環境は、PowerShell v4 / x64。

準備

インストール

適当なフォルダを作り、NuGetでMagick.NETのx64 or x86を環境にあわせてもってくる。
PS C:\> mkdir c:\opt\magick
PS C:\> cd c:\opt\magick
PS C:\opt\magick> Invoke-WebRequest https://nuget.org/nuget.exe -OutFile nuget.exe
PS C:\opt\magick> .\nuget install Magick.NET-Q16-x64
NuGet的にはAnyCPUがあるので、何も考えずにAnyCPUを選びたくなるところではあるが、それはNG。というのも、AnyCPU版はx86とx64のDLLをEmbedded Resourceでかかえる形になっていて、必要に応じてロードしているっぽいのだが、これがAdd-Typeでうまくロードできない。x64やx86はプラットフォーム依存DLLがそのまま置いてあるのでOK。

アセンブリのロード

Add-Typeするだけ。これはさすがにスクリプト化しておいたほうが便利かもね。
PS C:\opt\magick> Add-Type -Path .\Magick.NET-*\lib\net40-client\Magick.NET-x64.dll

イメージの読み書き

イメージの読み込み

とにかくここから始まる。これはMagickImageインスタンスを作ればOK。
PS c:\temp> $image = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
ただし、ワーキングディレクトリはPowerShellのものではないので、フルパスで指定するのが無難。クラス名はNew-O[TAB] MagickI[TAB]で補完できるのでおすすめ。

イメージの書き込み

後述の加工作業を行ってから、.Write(path)で書く。

PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

基本的に「New-Object」「何かして」「Write」の組み合わせ。

フォーマット指示が必要な場合、Writeの前に入れる。
PS C:\temp> $image.Format = 'png'
PS C:\temp> $image.Write('c:\temp\sample-out.png')
とか、
PS C:\temp> $image.Format = 'jpeg'
PS C:\temp> $image.Quality = 99
PS C:\temp> $image.Write('c:\temp\sample-out-q99.jpg')
とか。

あとは
PS c:\temp> $image.Dispose()
で後片付け。インタラクティブ操作時はpowershellごと消してもいいと思うけど、ループ時とかは注意。

情報の表示

属性の表示

$imageと打てば、それだけで情報がでてくる。Width, Heightなど。これだけでもわりと便利。ただ、計算するプロパティが含まれているせいか、全部出すと重いのでループするのはおすすめしない。表示はこんな感じ(全部ではない):


EXIF情報の表示

$image.GetExifProfile().Values でOK。
PS C:\temp> $image.GetExifProfile().Values
 DataType                       IsArray                           Tag Value
 --------                       -------                           --- -----
    Ascii                         False              ImageDescription
    Ascii                         False                          Make SONY
    Ascii                         False                         Model NEX-6
    Short                         False                   Orientation 1
 Rational                         False                   XResolution 350
 Rational                         False                   YResolution 350
    Short                         False                ResolutionUnit 2
    Ascii                         False                      Software NEX-6 v1.01
    Ascii                         False                      DateTime 2015:05:24 10:46:23
    Short                         False              YCbCrPositioning 2
Undefined                          True                       Unknown {80, 114, 105, 110...}
 Rational                         False                  ExposureTime 0.01
 Rational                         False                       FNumber 13
    Short                         False               ExposureProgram 2
    Short                         False               ISOSpeedRatings 100
(以下略)
PS C:\temp> $image.GetExifProfile().Values.Where({$_.Tag -eq 'FocalLengthIn35mmFilm'}).Value
24

1ファイル系の処理

1ファイルに対する処理をこの画像を使って行っていく。


回転(変換と保存)

回転はRotate()。

PS c:\temp> $image = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS C:\temp> $image.Rotate(90)
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')


(多分)EXIFにしたがって回転してくれるAutoOrient()ってのもある。便利ね。

切り出し

切り出しはCrop()。Gravityはどこからひっぱってくるかの指示。
PS c:\temp> $image = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS c:\temp> $image.Crop(200, 200, [ImageMagick.Gravity]::Center)
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

リサイズ

Resize()かResample()かScale()かThumbnail()。この使い分けは難しいのだけど、とりあえず無難そうなResizeだけ説明。

そのResize()もパラメータをいくつか選べる。
PS C:\temp> $image.Resize

OverloadDefinitions
-------------------
void Resize(int width, int height)
void Resize(ImageMagick.MagickGeometry geometry)
void Resize(ImageMagick.Percentage percentage)
void Resize(ImageMagick.Percentage percentageWidth, ImageMagick.Percentage percentageHeight)

一番簡単なやつがwidth, heightを指定するやつ(New-ObjectとWriteはここの説明では省略)
PS C:\temp> $image.Resize(200, 100)
ただし、アスペクト比を維持するので、(200, 100)にはならない。ここに入る大きさになる。



パーセンテージ指定が次に簡単(書き方がイマイチ...)
PS C:\temp> image.Resize((New-Object ImageMagick.Percentage 50))


で、もっともいろいろできるのが、MagickGeometry指定パターン。ImageMagickをコマンドで使っている人ならおなじみの、例の記法が使える。例えば、アスペクト比を無視する!を付与したスタイル。
PS C:\temp> $image.Resize((New-Object ImageMagick.MagickGeometry '200x200!'))


このあたりの挙動はImageMagickそのものの話なので、詳しくは公式ドキュメント(ImageMagick v6 Examples --
Resize or Scaling (General Techniques)
)を参照。なお、日本語でまとめてくれてる人もいる(ImageMagickでリサイズする方法

複数ファイル系の処理

ここからはこの2つの画像を例に説明。

結合

結合は、MagickImageCollectionを作ってimageを追加し、AppendHorizontally()かAppendVertically()を呼ぶ。
PS C:\temp> $image1 = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS C:\temp> $image2 = New-Object ImageMagick.MagickImage c:\temp\sample2.jpg

PS C:\temp> $col = New-Object ImageMagick.MagickImageCollection
PS C:\temp> $col.Add($image1)
PS C:\temp> $col.Add($image2)

PS C:\temp> $image = $col.AppendVertically() # あるいは $col.AppendHorizontally()  
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

重ねる

結合とだいたい同じノリだけども、重ねることも可能。
PS C:\temp> $image1 = New-Object ImageMagick.MagickImage c:\temp\sample.jpg
PS C:\temp> $image2 = New-Object ImageMagick.MagickImage c:\temp\sample2.jpg

PS C:\temp> $col = New-Object ImageMagick.MagickImageCollection
PS C:\temp> $col.Add($image1)
PS C:\temp> $col.Add($image2)

PS C:\temp> $image = $col.Evaluate([ImageMagick.EvaluateOperator]::Add)
PS C:\temp> $image.Write('c:\temp\sample-out.jpg')

ここまで書いておいて例が悪いな。

etc, etc...

あとはBlurとか、さまざまなフィルタがあるのだけど、きりないからこのへんでやめ。ImageMagickは加工は得意なので、できることはたくさんある。

他のものを知りたかったら、$image.で[TAB]してみたり、MagickNetのドキュメントとか、ImageMagickのコマンドラインオプション(ImageMagick: Command-line Options)を参照。

おまけ: 統計情報の表示

最後に一応、需要はなさそうだけど、これをやるために使い始めたので説明。平均とかの取り方。

Statistics()を呼んで、Composite()あるいはGetChannel(チャンネル)を呼ぶ。
先に
$image.Grayscale([ImageMagick.PixelIntensityMethod]::Lightness)
を呼んでおくのも便利かも。
PS C:\temp> $stat = $image.Statistics()
PS C:\temp> $stat.Composite()

Channel           : Composite
Depth             : 1
Entropy           : 0.966856332282486
Kurtosis          : -1.25816857805541
Maximum           : 65535
Mean              : 27115.482213115
Minimum           : 0
Skewness          : 0.0779930273889325
StandardDeviation : 17685.6919467067
Sum               : 27115.482213115
SumCubed          : 45922010656938.6
SumFourthPower    : 2.14361048522477E+18
SumSquared        : 1049386363.99743
Variance          : 312783699.633806

PS C:\temp> $stat.GetChannel([ImageMagick.PixelChannel]::Blue)

Channel           : Blue
Depth             : 8
Entropy           : 0.967346949241304
Kurtosis          : -1.26933689965117
Maximum           : 65535
Mean              : 28404.2418122061
Minimum           : 0
Skewness          : 0.0995661483530123
StandardDeviation : 18559.3929293196
Sum               : 28404.2418122061
SumCubed          : 52904690206274
SumFourthPower    : 2.59600361552516E+18
SumSquared        : 1151252018.83115
Variance          : 1151252018.83115

Springありで@TheoryしたいときはSpringClassRule/SpringMethodRuleを使う

タイトルの通り。

@RunWith(SpringJUnit4ClassRunner.class) を指定していると JUnit4 で @Theory とか @Parameterized したいときに困るわけだけども、SpringClassRule と SpringMethodRule を使えば、RunWithにTheories.classとかを指定できる。

サンプル的なものはこちら。

参考:Spring Framework Reference Documentation 14.5.8 TestContext Framework support classes

比較的新しいらしい@FromDataPointsも使ってみたよ。

なお、Spring 4.2からのようなので注意。
testCompile 'org.springframework:spring-test:4.2.0.RELEASE'

2015-08-08

Caliburn.Micro + AutofacでWPFプロジェクトを開始する方法のメモ

ひとつひとつ解説すると長くなるので、使い慣れた構成で淡々とプロジェクトを開始できるようにというメモにする(更新するかも)

概要

以下の構成でプロジェクトを開始する。
  • Caliburn.Micro (MVVMライブラリ) → ガイド
  • Autofac (DIコンテナ) → ガイド
  • Autofac.AttributedComponent (Autofacへのコンポーネント収集)
  • ReactiveProperty + Rx(Model/ViewModel間の連携)

プロジェクトの作成とパッケージインストール

  • WPFプロジェクトを作る
  • 以下をInstall-Package
    • Caliburn.Micro
    • Autofac.AttributedComponent
    • ReactiveProperty

初期View / ViewModelの作成

ViewModels/ShellViewModel.csにViewModelを作成、Views/ShellView.xamlにWindowを作成。
(フォルダは切らなくてもいい: View / View Model Naming Conventionsを参照)

ShellViewModelはCaliburn.Micro.PropertyChangedBaseを継承し、Component attributeを付与する(Scope=ComponentScope.SingleInstanceにするかは好みで)


ShellView.csのクラスにも[Component]をつける。
(つけなくてもいいが、[Resource]によるフィールド・インジェクションが可能になるので、これによってビュー向けのシングルトンインスタンスを受け取ると便利。)

AppBootstrapperの作成

Caliburn.Micro初期化用クラスをBootstrapperBaseを継承して作る。

Customizing The Bootstrapper を参照。Caliburn.Micro.AutofacBootstrap がそこそこやってくれるのだけど、ちょっとばかり微妙なので、Autofacへのつなぎは以下で済ませる。



App.xamlの設定

以下のようにする。StartupUriは削除する。


MainWindow.xaml(.cs)の削除

いらないので削除。

おまけ: お試し

試したかったら、まぁこんな感じで(Basic Configuration, Actions and Conventionsより)

ShellView.xaml追記


ShellViewModel.cs


2015-08-04

groovyshでJavaのクラス情報を参照して適当にコード片を生成する

IDEがやってくれるような一般的なコード生成ではないが、規則のあるコードをちょちょいと生成したりしたいときがある。Annotation Processingでアノテーション見ながらというのが王道だろうが、そこまででもない使い捨てな感じにコードをGroovyを使って生成する方法。

groovyshの起動

GroovyのREPLであるところのgroovyshをまず起動する。追記: なお、IntelliJ IDEAであればTools > Groovy Consoleで起動できるGroovyで処理できるので、いきなり次の作業に飛んで問題ない。

単純なケースであれば、クラスパスを指定して、groovyshを起動する。
$ groovysh -cp out/production/javaclasstest

普通はさらさらっと指定できるほど単純ではないと思うが、Gradleからgradle-groovysh-pluginを使ってgroovyshを起動すれば、必要なものをロードしておいてくれる。

基本的には、以下の変更をbuild.gradleに加えた後「./gradlew -q --no-daemon shell」で起動すればOK。
  • buildscriptのdependenciesにcom.tkruse.gradle:gradle-groovysh-pluginを追加する。
  • 'java'と'com.github.tkruse.groovysh'をapply pluginする。
例としてはこんな感じ。

apply plugin: 'java'
apply plugin: 'com.github.tkruse.groovysh'

buildscript {
  repositories {
      jcenter()
  }

  dependencies {
      classpath 'com.tkruse.gradle:gradle-groovysh-plugin:1.0.7'
  }
}


ただし、gradle.propertiesに「org.gradle.daemon=true」が書いてあると「Do not run with gradle daemon (use --no-daemon)」と言われて起動できない。--no-daemonをつけろと言うのだが、つけてもダメ。多分チェック方法が間違っている。Issueに登録しておいたが、直るまでは実行するときはファイルにtrueを記述していない状態にするしかない。

[2015/8/26追記] warningが出つつも起動するようにしてもらえたので、ちゃんと-q --no-daemonで起動すればOK。

リフレクションを使って適当に作る

あとはjava.lang.Classjava.lang.reflect.Methodを見ながら、適当にコードを生成する。GroovyなのでgetHogeはhogeでいいし、Rubyちっくに適当につなげれば生成できるのでラク。

たとえば、こんな感じ。
$ ./gradlew shell -q
(略)
groovy:000> cls = com.example.HelloJavaBuilder
===> class com.example.HelloJavaBuilder
groovy:000> cls.methods.findAll { it.declaringClass == cls }.collect { ".${it.name}()" }.sort().join("\n")
===> .build()
.fuga()
.hige()
.hoge()
groovy:000> 

ポイントとしては、import なんちゃらと打つとパッケージ名の補完が効くので、いったんimportしちゃうなり、補完までしてimportを書き換えるなどするとパッケージ/クラス名の入力が楽。

Tips: GroovyのMetaClassを活用する

setterのsetを外したところを取りたい、みたいな話だと、普通にやるとこんな感じだと思う。
groovy:000> cls.methods.findAll { it.declaringClass == cls && it.name =~ /set/ }.sort().
collect { it.name.replaceAll(/set/, '').replaceAll(/^./) { it.toLowerCase() } }

しかし、GroovyのmetaClassを見ると、setter/getterはset/getを外した形でpropertiesに入っているので、こっちを使うとかなり楽になる(classもあるけど、そこはうまいこと外す)
groovy:000> cls.metaClass.properties.collect{ it.name }

もし役に立つ機会があればどうぞ。

2015-07-21

OpenCvSharpで表紙の折り目を検知してみた

いわゆる本の自炊をする際、表紙は切らず長尺読みをし、それを分割して使う。その分割作業をサポートするために作ったのがKiritoriMageなのだけど、この切り取る座標を決定するのを自動化できるんじゃね?ってことでやってみた。



詳しくは以下に記述するが、要は折られている以上折り目が存在するので、それを検知すれば、どこで切ればいいかわかるという作戦。その折り目を見つけるにも、印刷されているほうを使うと大変なので、一緒にスキャンされる裏側を使うことにした。

それを実装したのがKiritoriMage 0.0.2で、切りたい画像とセットになる裏側を一緒にドラッグアンドドロップしてあげればこの機能が動作する。試してみたい方はこちらからどうぞ。
https://github.com/sunnyone/kiritorimage/releases/tag/v0.0.2


処理の詳細

OpenCvSharpの使い方例として、処理内容を解説していく。わかりやすさのために、順番を差し替えたり、定数をリテラルにしたりしている。また、OpenCvSharpなので実際の呼び出しとは異なるが、解説が詳しいので、opencv.jpのC/C++のコードの説明をリンクしている。

今よりずっとWPFを使えないときに書いたために、ソースはだいぶきれいではないのでKiritoriMageを読むのはお勧めしないが、この処理自体はFoldDetectUtil.csにある。

今回の例に使う画像を縮小(+α)したものがこちら。実際の表紙を使うと面倒なので、適当な表紙を作った。
この偽表紙は実際の表紙と異なり普通紙でガサガサなので、裏にはちょっとNRをかけてある。


Phase1 折り目候補の洗い出し

画像に手を加えることで、折り目らしき部分を抽出する。

1-1. 画像の読み込み

簡単。Matクラスのコンストラクタにファイル名を渡すだけ。
var matSrc = new Mat(backFilename);

1-2. AdaptiveThresholdによる2値化

これが全体の中で一番肝だと言っていい。最初使ったときは感動した。

ただの2値化は固定値か全体の値を見て白か黒かにするという感じだけど、AdaptiveThresholdを使うとピクセルの周りを見て白か黒か選んでくれる(参考: OpenCvSharpをつかう その15(適応的閾値処理))。全体を見てしまうとこの紙の裏の画像はほとんど差がないので、自動的にやろうとするとどうしても「全部白!」or「全部黒!」となってしまうのだけど、折り目境界部分は比較的違いがはっきりしているので折り目をはっきりと浮かび上がらせてくれる。

コード的には、CvtColorでグレースケールに変換した後、AdaptiveThresholdを呼べばOK。KiritoriMageでは、blockSizeは幅の1/200を奇数にしたもの(4000なら21)としている。
var mat = matSrc.CvtColor(ColorConversion.BgrToGray)
    .AdaptiveThreshold(255, AdaptiveThresholdType.MeanC, ThresholdType.Binary, blockSize, 1);

後述の輪郭検出の都合が良いので、反転しておく。
mat = ~mat;

1-3. 高さ1にリサイズ後、平滑化をかけて2値化

折り目は縦一線にできるので、Resizeで思い切って高さ1にする。この処理ではInterpolation.Areaが一番よかった。
Cv2.Resize(mat, mat, new Size(matSrc.Width, 1), interpolation: Interpolation.Area);
(高さ1ピクセルでは見えないので、この状態で高さを戻すリサイズをかけた状態はこんな感じ)

そのままでは黒と白が細かく散っているので、平滑化する。輪郭を残してくれそうなので、MedianBlurをチョイス。KiritoriMageではksizeはさっきのblockSizeと一緒にしている(動かしてて困らない数値だっただけで、一緒にしたいという意図はない)。
mat = mat.MedianBlur(ksize)

(高さ1は見えないので、高さを戻した状態の画像)

終わったら再度2値化する。ここでは大津の2値化を使った。
mat = mat.Threshold(0, 255, OpenCvSharp.ThresholdType.Otsu);

(再び2値化後の高さを戻した状態の画像)

1-4. 高さを作り輪郭検出

1次元のデータなのでこのまま回して分割していってもいいのだけど、だるいので高さを作ってOpenCVの輪郭検出FindContoursにまかせて、その輪郭を囲む矩形の座標をもらう。…なんて無駄なのでしょう!遅くて使えなかったら本気出す。まぁここでFindCoutoursの例を出すために使ったと言っても過言ではない。なお、FindContoursはmatを破壊するので注意。
Cv2.Resize(mat, mat, new Size(matSrc.Width, 3), interpolation: Interpolation.Cubic);
var rects = mat
    .FindContoursAsArray(ContourRetrieval.External, ContourChain.ApproxSimple)
    .Select(x => Cv2.BoundingRect(x))
    .OrderBy(x => x.X)
    .ToArray();

(選択できたものをCv2.Rectangleで書いた画像)

ここで折り目候補ができたが、ここまでで90%、成否が決まっているといっても過言ではない。候補が大量にできていてはうまくいかないし、そもそも各折り目がどれか検出されなかった時点でアウト。

本当はここで処理を完了したかったのだけど、そんな調子なので、折り目だけを検出するようにパラメータを構成するのは難しく、どれかの折り目が検出されなくなってしまうことがほとんどなので、候補から適切なものを選択することにした。

Phase2 折り目候補から折り目4つを選択

「表紙の紙の折り目」である、という特徴を使って、一番選択肢として妥当なものを選択する。

2-1. 折り目候補の一覧から4つを選ぶ全ての組み合わせの算出

いわゆる順列/組み合わせ。候補から4つを選ぶ組み合わせを列挙する。コードは省略。そこまで長くはないし、実際自前で実装しているんだけど、NuGetでパッとインストールできるこの手の処理が集まってるライブラリの都合のいいやつがないのよね。LINQに入っていてもおかしくない汎用さなんだけど。

2-2. 座標、幅をピクセル数から全体幅との比率に変換

まず各組み合わせについて、画像のサイズは毎回違っていて扱いにくいので、全体の幅で割って割合にする。書いていて思ったが先にやっておいたほうが計算量は少ないね。
var widthRatios = rectCands.Select(r => new { 
    Left = r.X / imageWidth, 
    Right = (r.X + r.Width) / imageWidth,
    Width = r.Width / imageWidth }).ToArray();

2-3. 幅が3%以上なら組み合わせを却下

組み合わせのひとつひとつを見て、折り目が太すぎるもの、具体的には全体幅の3%越えが存在するものはおかしいのでその組み合わせは却下する。過去データを見る限り、1%くらいだったので、余裕を持って3%。
rectSets = rectSets.Where(r => r.WidthRatios.All(x => x.Width < 0.03)).ToArray();

2-4. 細い同士、太い同士の割合(thinRatio, fatRatio)を計算

表紙の紙の特徴として、基本的に表紙と裏表紙(「太いほう」)は同じ幅だし、折り込まれた表紙の内側と裏表紙の内側(「細いほう」)も同じ幅のはず。そのため、表紙の幅/(表紙の幅+裏表紙の幅)という感じで太いほう、細いほうそれぞれの片側が占める割合を計算して(thinRatio, fatRatio)とする。
double leftThin = widthRatios[0].Left;
double rightThin = 1 - widthRatios[3].Right;
double thinRatio = leftThin / (leftThin + rightThin);

double leftFat = widthRatios[1].Left - widthRatios[0].Right;
double rightFat = widthRatios[3].Left - widthRatios[2].Right;
double fatRatio = leftFat / (leftFat + rightFat);
その(thinRatio, fatRatio)と(0.5, 0.5)の距離を計算して、一番近かったものを選んで、これを答えとする。
double thinFatDistance = Math.Sqrt(Math.Pow(fatRatio - 0.5, 2) + Math.Pow(thinRatio - 0.5, 2));

var bestMatchRectSet = rectSets.OrderBy(x => x.ThinFatDistance).First();

これで選ばれたものがこちら。


あとは切るX座標として0, 1, 3番目の右側を選んで終了。

いくつか手法を試して、結果的にわりとシンプルなやりかたに落ち着いた。太いほう:細いほうの比はあまり極端ではないはずなので、そこを基準にフィルタしてもいいかなと思っているけども、使い込んでいないのでまぁ様子を見てという感じ。一晩でやれたらいいかなくらいだったものを、だいぶ頑張って実装してしまった感があるので、ここで一区切りかな。

2015-07-13

bsdtarでzip展開時の先頭ディレクトリを削る

zipに限らずアーカイブにはこんな形でファイルが入っていることがある。
hoge-0.1.2/README.md
hoge-0.1.2/configure
hoge-0.1.2/src/
hoge-0.1.2/src/main.d

これをunzipで普通に展開すると、当然ながらhoge-0.1.2ができるが、バージョンが入っているのがうれしくないことがある。ここで、tarの「--strip-components」オプションと、zipを展開できるbsdtarを組み合わせることで、hoge-0.1.2を外して展開することができる。
$ bsdtar xvf hoge-0.1.2.zip --strip-components 1
x README.md
x configure
x src/
x src/main.d

どうでもいいTipsでした。