function varargout = print (varargin) %PRINT Transparent shim: render PNGs through gnuplot's cairo terminal. % % The model writes ordinary MATLAB - print(f,'plot.png','-dpng') - and must % keep writing ordinary MATLAB, because whatever it writes is what the user % runs. But gnuplot's plain `png` terminal does not anti-alias, and draws % every line as a 1-pixel stroke regardless of LineWidth. On a steep curve % that needs several vertical pixels per column it leaves gaps, so a 4 Hz % sine came out as a field of disconnected dashes rather than a wave. % % `pngcairo` anti-aliases and renders the same LineWidth as a continuous % stroke. Swapping it in here means the picture is right without the code % being wrong: -dpngcairo is Octave-only and would fail in MATLAB, so it % must never appear in anything the model writes. % % The layout goes the same way. Canvas size, type size and where the key % sits are properties of this renderer, not of the analysis, so they are % set here rather than in lines the model would have to remember to write. % Three fixes, each explained at its helper below: % % __owuitools_canvas__ a big canvas WITHOUT print's font scaling % __owuitools_type__ label type the size gnuplot reserves for % __owuitools_legends_below__ the key under the axes, not over the data % % Every one of them is undone before this function returns: the figure is % handed back exactly as the script left it, because the script may draw % into it again and print it a second time. % % This file lives outside the working directory and is put on the path % ahead of Octave's own print, so the user's files are untouched. args = varargin; has_device = false; has_size = false; has_resolution = false; wants_png = false; % The layout work below is about pixels, so it covers every picture the % tool can hand back - .png, .jpg, .gif - while the cairo swap is about % one gnuplot terminal and stays with PNG alone. raster_ext = {'.png', '.jpg', '.jpeg', '.gif'}; raster_dev = {'-dpng', '-djpg', '-djpeg', '-dgif'}; wants_raster = false; for k = 1:numel (args) if (ischar (args{k})) a = args{k}; if (strncmpi (a, '-S', 2)) has_size = true; elseif (strncmpi (a, '-r', 2) && numel (a) > 2) has_resolution = true; elseif (strncmpi (a, '-d', 2) && ! strncmpi (a, '-debug', 6)) has_device = true; if (any (strcmpi (a, raster_dev))) wants_raster = true; end if (strcmpi (a, '-dpng')) args{k} = '-dpngcairo'; wants_png = true; end elseif (a(1) != '-') [~, ~, ext] = fileparts (a); if (any (strcmpi (ext, raster_ext))) wants_raster = true; wants_png = wants_png || strcmpi (ext, '.png'); end end end end % print() also infers the device from the file extension, so a bare % print(f,'plot.png') has to be caught too. if (wants_png && ! has_device) args{end+1} = '-dpngcairo'; end % Paper inches become pixels at the print resolution, which defaults to % 150 dpi: the JPEG terminal took the 1100 pt canvas set below and made a % 2292 px image of it, with 10 pt type marooned in the middle. pngcairo % ignores the setting, so pinning it to 72 costs nothing there and makes % one inch mean one hundred points wherever it is read. if (wants_raster && ! has_size && ! has_resolution) args{end+1} = '-r72'; end shim_dir = fileparts (mfilename ('fullpath')); rmpath (shim_dir); unwind_protect undo = {}; if (wants_raster) fig = __owuitools_figure__ (args); undo = [__owuitools_canvas__(fig, has_size), ... __owuitools_type__(fig), ... __owuitools_legends_below__(fig)]; end unwind_protect if (nargout > 0) [varargout{1:nargout}] = print (args{:}); else print (args{:}); end unwind_protect_cleanup for k = numel (undo):-1:1 try set (undo{k}{1}, undo{k}{2}, undo{k}{3}); catch end end end_unwind_protect unwind_protect_cleanup addpath (shim_dir, '-begin'); end_unwind_protect end function fig = __owuitools_figure__ (args) % print(f,...) names its figure; print('plot.png',...) means the current one. fig = []; try for k = 1:numel (args) if (! ischar (args{k}) && isscalar (args{k}) && ishghandle (args{k})) fig = ancestor (args{k}, 'figure'); return; end end if (! isempty (get (0, 'currentfigure'))) fig = gcf (); end catch fig = []; end end function undo = __owuitools_canvas__ (fig, has_size) %__OWUITOOLS_CANVAS__ A bigger picture without doubling the type with it. % % -S is the obvious way to ask for a 1100 px canvas and the wrong one. % print.m reads it as "a six-inch figure blown up to 1100 px" and scales % every font in the figure by % % width / 6 / screenpixelsperinch (72 here, so width/432) % % which at the old -S900,675 was 2.08x: the 10 pt axes font was drawn at % 16.7 pt and the 11 pt title at 18.3 pt on a canvas only 900 px wide. % That is what pushed titles off the right edge, sat axis labels on top % of tick labels, and ran bar-chart category names into each other. A % bigger -S never helped, because the type grew with the canvas. % % Sizing by paper instead takes the other branch of print.m, where the % canvas is paperposition x 72 and the scale factor stays 1 - so the type % keeps the size Octave gave it and a bigger canvas buys real room. % % The shape follows the figure: a script that made a wide figure gets a % wide PNG. Only the width is fixed, and only when the caller did not ask % for a size itself. undo = {}; if (has_size || isempty (fig)) return; end try width = 1100; pos = get (fig, 'position'); aspect = 0.75; if (numel (pos) == 4 && pos(3) > 0 && pos(4) > 0) aspect = min (1.5, max (0.4, pos(4) / pos(3))); end undo = {{fig, 'paperunits', get(fig, 'paperunits')}, ... {fig, 'paperposition', get(fig, 'paperposition')}, ... {fig, 'paperpositionmode', get(fig, 'paperpositionmode')}}; set (fig, 'paperunits', 'inches'); set (fig, 'paperposition', [0, 0, width/72, round(width*aspect)/72]); set (fig, 'paperpositionmode', 'manual'); catch end end function undo = __owuitools_type__ (fig) %__OWUITOOLS_TYPE__ Axis labels the size gnuplot reserved room for. % % gnuplot lays a plot out in character units of the TERMINAL font, and % Octave sets that terminal font from the axes font size - 10 pt - while % drawing axis labels at 11, because labelfontsizemultiplier is 1.1. The % gap is small and the result is not: the space reserved below the tick % labels is a 10 pt line, the xlabel drawn into it is an 11 pt line, and % it lands on the tick row. Same story on the left for the ylabel. % % Dropping the label multiplier to 1 makes the two agree exactly, which % is the whole fix - verified against gnuplot directly: same plot, same % key, xlabel at 11 pt overlaps the ticks and at 10 pt clears them. % % The title keeps its 1.1 and its bold weight. It sits in the top margin % with nothing under it to collide with, and it is the one piece of text % a reader should find first. % % With that tie in place the size can be chosen on its merits, and 10 pt % - Octave's default, for a 560 px figure on a screen - is too small on a % 1100 px canvas that a chat window then shows at 700. 12 pt is about % where it reads at that width and still leaves six category names room % side by side. A script that set a font size itself keeps it: that is % what fontsizemode records. undo = {}; if (isempty (fig)) return; end try ax = findall (fig, 'type', 'axes'); for k = 1:numel (ax) if (strcmp (get (ax(k), 'tag'), 'legend')) continue; end undo{end+1} = {ax(k), 'labelfontsizemultiplier', ... get(ax(k), 'labelfontsizemultiplier')}; set (ax(k), 'labelfontsizemultiplier', 1); if (strcmp (get (ax(k), 'fontsizemode'), 'auto')) undo{end+1} = {ax(k), 'fontsize', get(ax(k), 'fontsize')}; set (ax(k), 'fontsize', 12); end end catch end end function undo = __owuitools_legends_below__ (fig) %__OWUITOOLS_LEGENDS_BELOW__ The key under the axes, not over the data. % % A legend inside the axes sits on top of what it is labelling: the key % is opaque here (see patch-legend-opaque.sh, and transparent is worse - % the lines then run through the text), so it hides the curves under it. % 'best' does not help either; this backend does not implement it and % warns, and every legend lands top-right regardless of what was asked. % % Below the axes nothing can collide with the data, because gnuplot % reserves the space for an outside key and shrinks the plot to fit it. % % The frame comes off with the move. gnuplot packs an outside key flush % against the x-axis label - a one-pixel gap, whatever the type size or % the label's own offset - so the box's top edge is drawn through the % descenders of that label and reads as an underline under it. Out on % the white margin, with no plot lines behind the text, the frame was % only decoration; without it the key is a caption under the plot. % (The fill is handled one layer down: patch-legend-opaque.sh keeps an % outside key transparent, because an opaque one erased those same % descenders outright - "the trigger edge" printed as "the triaaer edae".) % % Two legends are left where they are. One a script deliberately put % outside already - 'eastoutside' - because that is a layout decision % rather than the default falling where it falls. And every legend in a % figure that has more than one plot in it: a subplot grid has no margin % under each plot to give away, only the gap before the next one's title, % and a key dropped into that gap lands on both. Inside is where they fit % there, and inside is where the opaque fill still applies. undo = {}; if (isempty (fig)) return; end try h = findall (fig, 'type', 'axes'); tags = get (h, 'tag'); if (! iscell (tags)) tags = {tags}; end if (sum (! ismember (tags, {'legend', 'colorbar'})) != 1) return; end for k = 1:numel (h) if (! strcmp (get (h(k), 'tag'), 'legend')) continue; end loc = get (h(k), 'location'); if (! ischar (loc) || ! isempty (strfind (lower (loc), 'outside'))) continue; end undo{end+1} = {h(k), 'location', loc}; set (h(k), 'location', 'southoutside'); undo{end+1} = {h(k), 'box', get(h(k), 'box')}; set (h(k), 'box', 'off'); end catch end end