MATLABからベクトルグラフィックスとして図を書き出す方法

MATLABで図を作った後、それを何らかの画像フォーマットに書き出して、Illustratorなどの画像処理ソフトで仕上げをして、論文に投稿、という作業の流れは非常に一般的だと思います。一旦ちゃんと図さえ作ってしまえば、後の工程は遊びみたいなものだから心配いらないと思うでしょう。私もそうでした。しかしこれが案外、厄介です。

一つの問題は図の中に透明度を指定したものが混じっているときに、それが出力ファイルに維持されているかどうかです。従来よく使われていたEPSへの書き出しは透明度を維持できません。問題はこうした細かいことについて公式のドキュメンテーションには記載がないことです。


# 実験

透明度（`FaceAlpha`, `EdgeAlpha`などで指定）を含む単純な図を作ってそれを書き出します。それをIllustratorとInkSpaceで開いてみてどのように表現されているか確認しました。

```matlab

f = figure

f.PaperType

f.PaperUnits = 'centimeters';
f.PaperPosition = [2 2 18 24];

f.Units = 'centimeters';
f.Position = [2 2 18 24];

f.Color = 'w';

axh = axes('Units','centimeters','Position',[2 2 10 10])

v1 = [2 4; 2 8; 8 4];
f1 = [1 2 3];
patch('Faces',f1,'Vertices',v1,'FaceColor','red','FaceAlpha',.3);

v2 = [2 4; 2 8; 8 8];
f2 = [1 2 3];
patch('Faces',f2,'Vertices',v2,'FaceColor','blue','FaceAlpha',.5);

title('Transparent patches')

axh.FontName = 'Arial'; 
% Arial font can be maintained. While default Helvetica will 
% be replaced with something else in Windows.

print(f,'-painters','test_print_behavior1','-dpdf')  % PDF
print(f,'-painters','test_print_behavior1','-depsc') % EPS
print(f,'-painters','test_print_behavior1','-dsvg')  % SVG

```
![download.png](https://qiita-image-store.s3.amazonaws.com/0/109898/a3abb596-0368-7791-ffb4-481721172023.png)




# 結果

### PDF in Illustrator CS5.5
- 高さと幅は正確に10 cm
- 透明度は　**Transparency Panel** の **Opacity** として維持されている
- 色はRGBで保持されている

### PDF in InkSpace 0.91
- 高さと幅は正確に10 cm
- 透明度は RGB color のA値（Alpha）として維持されている。 Opacity は 100%のまま。
- 色はRGBで保持されている

### SVG in Illustrator CS5.5
- 高さと幅は 13.335 cm (本来10 cm）
- 透明度は **Appearance Panel** の **Opacity** として維持されている
- 色はRGBで保持されている

### SVG in InkSpace 0.91
- 高さと幅は 10.6680 cm (本来10 cm）
- 透明度は RGB color のA値（Alpha）として維持されている。 Opacity は 100%のまま。
- 色はRGBで保持されている

### EPS in Illustrator CS5.5
- 高さと幅は正確に10 cm
- 透明度は失われて薄い色のベタ塗りで代用されている
- 色はRGBで保持されている (読み込み時に選択画面が出た)

### EPS in InkSpace 0.91
- ファイルを開くことができなかった

### Canvas X 16
- SVGを開くことが出来ない
- PDFの透明度を再現することが出来ない

# 考察

以上から、**編集可能なベクトルグラフィックスとして透明度を含む図を書き出す手段**は、いろいろ選択肢がありそうに見えて、ほぼこれひとつしかありません。

```matlab

print(f,'-painters',filename,'-dpdf')  % PDF

```
+ `f`は、figureのハンドル（`matlab.ui.Figure` class）

+ `'-painters'` は描画エンジンを指定して、ベクトルグラフィックスとしての書き出しを指定する。もう一つの選択肢の`'-opengl'`を選ぶとビットマップ画像として書き出され、その後の工程で部品を個別に修正することが出来ないので注意。

+ `filename`はファイル名の文字列

+ `'-dpdf'`はPDF書き出しを指定。


# 未解決問題

## FontName問題

また、補足ですが、`'FontName'`プロパティは私の環境のデフォルトではHelveticaになっていますが、このフォントがWindowsに内蔵されていないため、Windowsで書き出す場合はこれをあらかじめArialなどに変えておく必要があります。そうしないと、後の工程で勝手に別のフォントで代用されてしまいます。

これですべてを網羅することは出来ませんが、大半のオブジェクトの文字については以下のコードでデフォルトの設定を変えてしまうことも出来ます。

```matlab

set(groot,'defaultAxesFontName',    'Arial');
set(groot,'defaultTextFontName',    'Arial');
set(groot,'defaultLegendFontName',  'Arial');
set(groot,'defaultColorbarFontName','Arial');
```
最初の引数としてgrootの代わりに、特定のfigureやaxesのオブジェクトを指定すれば、デフォルトの指定範囲を限定することが出来ます。

## 特殊文字を用いるとPDF中で文字列がベクトルグラフィックスに置換される問題

論文などでは "0–200" というように**範囲を示す場合、ハイフンではなくen-dashを用いるのが正式**です。よく見るとハイフンよりも少し長いのがわかります。

Windowsでは `Alt`を押しながら`0150`、Macでは`Option`+`-`で、en-dashが打てます。

```matlab
sprintf('0%s200',char(8211))
```

これをMATLABの図の上でも実現しようとすると、上の例のように`char(8211)`で実現できますが、なんと、このようにしてせっかく図の上に en-dashを用いて文字を書いても、PDFで書き出す時に、en-dashを含む文字列のみベクトルグラフィックスに置換されてしまいます。PDF画面で見ると他の文字よりも若干大きさや形が違って見えるでしょう。

結局、このような特殊文字については、PDF書き出しの後で手動で入力するように対策がないようです。en-dash以外に正確にどの文字がこれに影響されるのかは分かっていません。


## 三角形の集合体問題

さて、`print(f,'-painters',filename,'-dpdf')`で一件落着、としたいところですが、R2017aの時点ではまだ問題が残ってます。`patch`オブジェクトや`surface`オブジェクトをこの方法でベクトルグラフィックスとして書き出すと、IllustratorやInkSpaceでは、**細かい三角形の集合体**として扱われます。つまり、ひとつのオブジェクトとしては扱われません。PDFの閲覧ソフトウェアの設定によっては、この小さな三角形の境界線が白い線として目に見えてしまいます。

### `patch`オブジェクト

patchオブジェクトについては、手動である程度対応できることが分かりました。

#### Illustratorの場合、

1. 小さな三角形をひとつ選ぶ
2. Select > Same > Fill & Strokeで同じ色の三角形をすべて選ぶ
3. Groupでグループ化
4. Isolate Selected Pathでこのグループだけを分離して表示
5. 結合するべきpatch オブジェクトの三角形をすべて選択する
2. Window > Pathfinder > Merge によって結合する
3. Exist Isolation Mode
4. Ungroup

#### InkSpaceの場合、

2. Ctrl + click で小さな三角形をひとつ選ぶ
2. Edit > Select Same > Fill Color
3. Path > Union

### `surface`オブジェクト

しかし`surface`オブジェクトについてはこの方法はうまく動作しないようです。MathWorksに改善要求を出しましたが、応じてくれるかどうかは分かりません。回避法としては、`surface`オブジェクトだけをビットマップ画像として書き出して後で貼り合わせるという手が一応あります。だいたいはうまくいきますが今のところ完全にピタリとはサイズが合わないようです。

#### `imagesc` を2D surfaceの代わりに用いる
二次元的に（`view(0,90)`など）表現されている`surface`オブジェクトであれば、昔から用いられていた問題回避法は、`imagesc`を代わりに使用することです。`imagesc`は二次元の`image`オブジェクトを作りますが、三次元方向の変数の大小を`colormap`を用いて表現できるので、`surface`や`surf`を使って作られたプロットの代用になります。

しかし、ひとつ問題があります。`surface` オブジェクトの場合、描画後に、`shading interp`を実行するだけで、簡単にinterpolation（補間、内挿）され、ギザギザのデータがなめらかな色表現として示されましたが、`imagesc`では生データがそのまま表示されてしまいます。interpolationはどうすればよいのでしょうか？`interp2`を使えば、一手間増えますが、ほぼ同じような結果を得ることが出来ます。

下の`surface2imagesc`は、既に存在している`surface`オブジェクトを`imagesc`を用いて`Image`オブジェクトに置き換えるための関数です。それほどテストしたわけではないのでまだ問題があるかもしれませんが、試した範囲では非常によく機能します。

`Image`オブジェクトに変換された後は、`PDF`に書き出しても、単一のオブジェクトとして扱われます。


```matlab

function im = surface2imagesc(sf,varargin)
%
% Converts a 2D (i.e, view(0,90) ) surface object to image with builtin
% imagesc. It is a known and long standing problem of MATLAB that surface
% plot with the height represented by color is beautiful, but quite
% difficult to be exported and handled afterwards. Exporting 2D surface
% plot as imagesc is a workaround for this.
%
% im = surface2imagesc(sf)
% im = surface2imagesc(____'Parameter', value)
% 
% INPUT ARGUMENTS
% sf          Surface object or array of surface objects
%
% OPTIONAL PARAMETER/VALUE PAIRS
% 'InterpMethod'  
%             'linear' (default)| 'nearest' | 'cubic' | 'spline'
%             The method of 2d interpolation (see
%             interp2 for details) The second and third element defined the
%             factor by which interpolation is carried out in X and Y
%             dimensions. {'linear',5,5} specifies 'linear' method with 5
%             times more resolution in X and Y.
%
% "InterpFactor'
%             [5,5] (default)
%             Two element vector. Defines the factor by which interpolation
%             is carried out in X and Y dimensions. [5,5] gives 5 times
%             more resolution in X and Y.
%
% 'KeepSf'    true (default) | false | 1 | 0
%             Whether to keep the original surf object after convertion.
%
% OUTPUT ARGUMENTS
% im          Image object 
%             DisplayName, Tag, YDir and CLim equals to those of
%             sf.DisplayName 
%
% Limitations 
% It is yet unknown if this funciton works any surface object that is
% presented in 2D.
%
% See also
% surface, imagesc, interp2
%
%
% Written by Kouichi C. Nakamura Ph.D.
% MRC Brain Network Dynamics Unit
% University of Oxford
% kouichi.c.nakamura@gmail.com
% 25-Apr-2017 14:25:12

p = inputParser;
p.addRequired('sf',@(x) all(all(isgraphics(x,'surface'))));
p.addParameter('InterpMethod','linear',@(x) iscell(x) && isrow(x) && ...
    numel(x) == 3 && ...
    ismember(x{1},{'linear' | 'nearest' | 'cubic' | 'spline'}) ...
    && (isscalar(x{2}) && x{2} > 0) && (isscalar(x{3}) && x{3} > 0));
p.addParameter('InterpFactor',[5,5],@(x) isreal(x) && isrow(x) && ...
    numel(x) == 2 && all(x > 0));
p.addParameter('KeepSf',true,@(x) isscalar(x) && x == 1 || x== 0);

p.parse(sf,varargin{:});


interpmethod = p.Results.InterpMethod;
interpfactor = p.Results.InterpFactor;
keepSf = p.Results.KeepSf;

im = gobjects(size(sf));
for i = 1:size(sf,1)
    for j = 1:size(sf,2)
        %NOTE it was hard to determine the right orientation. X and Y can
        % be flipped over or interp2 and imagesc issues an error.
        % Below was just by trial and error approach
        
        if all(sf(i,j).XData(1,1) == sf(i,j).XData(:,1))
            swap = true;
            xdata = sf(i,j).XData(1,:); 
        else
            swap = false; % WORKING
            ydata = sf(i,j).XData(:,1); 
        end
        
        if all(sf(i,j).YData(1,1) == sf(i,j).YData(:,1))
            assert(swap == false)
            xdata = sf(i,j).YData(1,:); 
        else
            assert(swap == true)
            ydata = sf(i,j).YData(:,1); 
        end

        zdata = sf(i,j).ZData;
        % cdata = sf(i,j).CData;
        
        axh = local_findparentaxh(sf(i,j));
        
        
        Xq = (xdata(1):diff(xdata(1:2))/interpfactor(1):xdata(end))';
        Yq = (ydata(1):diff(ydata(1:2))/interpfactor(2):ydata(end));
        
       
        if swap
            V = interp2(xdata,ydata,zdata,Xq,Yq,interpmethod);
        else
            V = interp2(xdata,ydata,zdata,Xq,Yq,interpmethod)';

        end
            
        
        ydir = axh.YDir;
        clim = axh.CLim;
        xlimit = axh.XLim;
        ylimit = axh.YLim;
        
        
        hold(axh,'on');
        if swap
            im(i,j) = imagesc(axh,Xq,Yq,V);
        else
            im(i,j) = imagesc(axh,Yq,Xq,V);
        end
            
        axh.YDir = ydir;
        axh.CLim = clim;
        
        im(i,j).UserData = sf(i,j).DisplayName;
        im(i,j).Tag = sf(i,j).Tag;
        
        axh.XLim = xlimit;
        axh.YLim = ylimit;
        
        if ~keepSf
            delete(sf(i,j));
            fprintf('A surface object has been deleted.\n')
        end
        
    end
end

end

%--------------------------------------------------------------------------

function axh = local_findparentaxh(sf)

if isgraphics(sf.Parent,'axes')
    
    axh = sf.Parent;
    
else
    %TODO need to be tested
    axh = local_findparentaxh(sf.Parent);
    
end

end

```