MATLABで図を作った後、それを何らかの画像フォーマットに書き出して、Illustratorなどの画像処理ソフトで仕上げをして、論文に投稿、という作業の流れは非常に一般的だと思います。一旦ちゃんと図さえ作ってしまえば、後の工程は遊びみたいなものだから心配いらないと思うでしょう。私もそうでした。しかしこれが案外、厄介です。
一つの問題は図の中に透明度を指定したものが混じっているときに、それが出力ファイルに維持されているかどうかです。従来よく使われていたEPSへの書き出しは透明度を維持できません。問題はこうした細かいことについて公式のドキュメンテーションには記載がないことです。
実験
透明度(FaceAlpha
, EdgeAlpha
などで指定)を含む単純な図を作ってそれを書き出します。それをIllustratorとInkSpaceで開いてみてどのように表現されているか確認しました。
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
結果
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の透明度を再現することが出来ない
考察
以上から、編集可能なベクトルグラフィックスとして透明度を含む図を書き出す手段は、いろいろ選択肢がありそうに見えて、ほぼこれひとつしかありません。
print(f,'-painters',filename,'-dpdf') % PDF
-
f
は、figureのハンドル(matlab.ui.Figure
class) -
'-painters'
は描画エンジンを指定して、ベクトルグラフィックスとしての書き出しを指定する。もう一つの選択肢の'-opengl'
を選ぶとビットマップ画像として書き出され、その後の工程で部品を個別に修正することが出来ないので注意。 -
filename
はファイル名の文字列 -
'-dpdf'
はPDF書き出しを指定。
未解決問題
FontName問題
また、補足ですが、'FontName'
プロパティは私の環境のデフォルトではHelveticaになっていますが、このフォントがWindowsに内蔵されていないため、Windowsで書き出す場合はこれをあらかじめArialなどに変えておく必要があります。そうしないと、後の工程で勝手に別のフォントで代用されてしまいます。
これですべてを網羅することは出来ませんが、大半のオブジェクトの文字については以下のコードでデフォルトの設定を変えてしまうことも出来ます。
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が打てます。
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の場合、
- 小さな三角形をひとつ選ぶ
- Select > Same > Fill & Strokeで同じ色の三角形をすべて選ぶ
- Groupでグループ化
- Isolate Selected Pathでこのグループだけを分離して表示
- 結合するべきpatch オブジェクトの三角形をすべて選択する
- Window > Pathfinder > Merge によって結合する
- Exist Isolation Mode
- Ungroup
InkSpaceの場合、
- Ctrl + click で小さな三角形をひとつ選ぶ
- Edit > Select Same > Fill Color
- 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
に書き出しても、単一のオブジェクトとして扱われます。
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