bers
bers

Reputation: 5813

Bitmap-render part of plot during vector-graphics export in MATLAB

I have a quite complex function that I plot using fsurf with a reasonably high MeshDensity (I cannot go much lower than the default, which is 35). Exporting this figure (saveas(gcf, 'file.pdf', 'pdf');) results in a 20-something MB pdf file of very nice quality, which nonetheless renders terribly slow. I would like to reduce the file size and, most importantly, the complexity of this pdf file, without exporting the whole plot (by which I mean the whole MATLAB figure) as a bitmap. How can I do that?

The perfect answer would explain how I can convert the surface plot (by which I mean, just the colored function surface on the white background) into a bitmap while maintaining the vectorized nature of axes and labels.

Update: Here's an example of such a plot.

enter image description here

Upvotes: 2

Views: 330

Answers (2)

davidm
davidm

Reputation: 1

Awesome solution, helped me a lot today.

I actually had a figure with subplots aka. multiple axes, therefore I allowed myself to adjust your function to handle multiple axes in a figure. In the interface you now have to provide the figure handle instead of the axes handle but everything else should work the same. I thought I'd share it here for anyone it might help in the future.

function BitmapRenderSubplots(Figure, KeepObjects, RelativePosition, Draft, Key)
% adapted from https://stackoverflow.com/questions/44286749/bitmap-render-part-of-plot-during-vector-graphics-export-in-matlab


if nargin < 2
    KeepObjects = [];
end
if nargin < 3
    RelativePosition = [0 0 1 1];
end
if nargin < 4
    Draft = false;
end
if nargin < 5
    Key = '';
end

Figure.Color = 'w';
drawnow;

Figure.Units = 'pixels';
FigureInnerWH = Figure.InnerPosition([3 4 3 4]);
PixelPosition = round(RelativePosition .* FigureInnerWH);

Children = Figure.Children;
cnt = 1;
for i=1:numel(Children)
    if IsType(Children(i),'axes')
        Axes(cnt) = Children(i);
        cnt = cnt+1;
    end
end

if isempty(Key)
    OverlayAxes = axes(Figure, 'Units', 'Normalized', 'Position', PixelPosition ./ FigureInnerWH);
    if Draft
        OverlayAxes.Box = 'on';
        OverlayAxes.Color = 'none';
        OverlayAxes.XTick = [];
        OverlayAxes.YTick = [];
        OverlayAxes.HitTest = 'off';
    else
        uistack(OverlayAxes, 'bottom');
        OverlayAxes.Visible = 'off';
    end

    for i=1:numel(Axes)
        origVisibility{i} = get(Axes(i).Children, 'Visible');
        Axes(i).CLimMode = 'manual';
        Axes(i).XLimMode = 'manual';
        Axes(i).YLimMode = 'manual';
        Axes(i).ZLimMode = 'manual';
    end
    setappdata(Figure, 'BitmapRenderOriginalVisibility', origVisibility);

    hManager = uigetmodemanager(Figure);
    [hManager.WindowListenerHandles.Enabled] = deal(false);
    set(Figure, 'KeyPressFcn', @(f, e) BitmapRenderSubplots(gcf, KeepObjects, RelativePosition, Draft, e.Key));
elseif strcmpi(Key, 'space')
    OverlayAxes = findobj(Figure, 'Tag', 'BitmapRenderOverlayAxes');
    delete(get(OverlayAxes, 'Children'));
    origVisibility = getappdata(Figure, 'BitmapRenderOriginalVisibility');
    Axes = Axes(1:end-1);
    for i=1:numel(Axes)
        OriginalVisibility = origVisibility{i};
        if isenum(OriginalVisibility)
            [Axes(i).Children.Visible] = OriginalVisibility;
        else
            [Axes(i).Children.Visible] = deal(OriginalVisibility{:});
        end
    end
else
    return;
end

if Draft
    return;
end

for i=1:numel(Axes)
    Axes(i).Visible = 'off';
end

KeepObjectsVisibility = get(KeepObjects, 'Visible');
[KeepObjects.Visible] = deal('off');

drawnow;
Frame = getframe(Figure, PixelPosition);

for i=1:numel(Axes)
    [Axes(i).Children.Visible] = deal('off');
    Axes(i).Visible = 'on';
    Axes(i).Color = 'none';
end

if numel(KeepObjects) == 1
    KeepObjects.Visible = KeepObjectsVisibility;
else
    [KeepObjects.Visible] = deal(KeepObjectsVisibility{:});
end

Image = imagesc(OverlayAxes, Frame.cdata);
uistack(Image, 'bottom');
OverlayAxes.Tag = 'BitmapRenderOverlayAxes';
OverlayAxes.Visible = 'off';
    
function isType = IsType(obj,type)
    try
        isType = strcmp(get(obj, 'type'), type);
    catch
        isType = false;
    end
end

end

Upvotes: 0

bers
bers

Reputation: 5813

This is my function BitmapRender, which Bitmap-renders part of the figure:

%% Test Code
clc;clf;
Objects = surf(-4-2*peaks);
hold('on');
Objects(2 : 50) = plot(peaks);
Objects(51) = imagesc([20 40], [0, 5], magic(100));
hold('off');
ylim([0 10]);
zlim([-10 15]);
Objects(1).Parent.GridLineStyle = 'none';
view(45, 45);
set(gcf, 'Color', 'white');
rotate3d on

saveas(gcf, 'pre.pdf');
BitmapRender(gca, Objects(2 : 3 : end));
% BitmapRender(gca, Objects(2 : 3 : end), [0.25 0.25 0.5 0.5], false);
saveas(gcf, 'post.pdf');

The function itself is pretty simple, except for the (re-)handling of visibility, as pressing the space key (after rotating, zooming etc) re-renders the figure.

function BitmapRender(Axes, KeepObjects, RelativePosition, Draft, Key)

if nargin < 2
    KeepObjects = [];
end
if nargin < 3
    RelativePosition = [0 0 1 1];
end
if nargin < 4
    Draft = false;
end
if nargin < 5
    Key = '';
end

Figure = Axes.Parent;
FigureInnerWH = Figure.InnerPosition([3 4 3 4]);
PixelPosition = round(RelativePosition .* FigureInnerWH);

if isempty(Key)
    OverlayAxes = axes(Figure, 'Units', 'Normalized', 'Position', PixelPosition ./ FigureInnerWH);
    if Draft
        OverlayAxes.Box = 'on';
        OverlayAxes.Color = 'none';
        OverlayAxes.XTick = [];
        OverlayAxes.YTick = [];
        OverlayAxes.HitTest = 'off';
    else
        uistack(OverlayAxes, 'bottom');
        OverlayAxes.Visible = 'off';
    end
    setappdata(Figure, 'BitmapRenderOriginalVisibility', get(Axes.Children, 'Visible'));

    Axes.CLimMode = 'manual';
    Axes.XLimMode = 'manual';
    Axes.YLimMode = 'manual';
    Axes.ZLimMode = 'manual';

    hManager = uigetmodemanager(Figure);
    [hManager.WindowListenerHandles.Enabled] = deal(false);
    set(Figure, 'KeyPressFcn', @(f, e) BitmapRender(gca, KeepObjects, RelativePosition, Draft, e.Key));
elseif strcmpi(Key, 'space')
    OverlayAxes = findobj(Figure, 'Tag', 'BitmapRenderOverlayAxes');
    delete(get(OverlayAxes, 'Children'));
    OriginalVisibility = getappdata(Figure, 'BitmapRenderOriginalVisibility');
    [Axes.Children.Visible] = deal(OriginalVisibility{:});
else
    return;
end

if Draft
    return;
end

Axes.Visible = 'off';

KeepObjectsVisibility = get(KeepObjects, 'Visible');
[KeepObjects.Visible] = deal('off');

drawnow;
Frame = getframe(Figure, PixelPosition);

[Axes.Children.Visible] = deal('off');
Axes.Visible = 'on';
Axes.Color = 'none';
if numel(KeepObjects) == 1
    KeepObjects.Visible = KeepObjectsVisibility;
else
    [KeepObjects.Visible] = deal(KeepObjectsVisibility{:});
end

Image = imagesc(OverlayAxes, Frame.cdata);
uistack(Image, 'bottom');
OverlayAxes.Tag = 'BitmapRenderOverlayAxes';
OverlayAxes.Visible = 'off';

end

Obviously, the solution is pixel-perfect in terms of screen pixels. Two pdf files (pre and post) look like this. Note that surface, image and some plot lines are bitmap rendered, but some other plot lines, as well as axes and labels are still vectorized.

enter image description here enter image description here

Upvotes: 1

Related Questions