From 287076a8d4f05dddeb2988f9d6cd587eff501cde Mon Sep 17 00:00:00 2001 From: Mark Mikkelsen Date: Tue, 7 Jun 2022 15:45:27 -0400 Subject: [PATCH] Update to v3.2.1 --- AlignSubSpectra.m | 2 - CoRegStandAlone/CoRegStandAlone.m | 60 ++- CoRegStandAlone/Seg.m | 12 +- GannetLoad.m | 4 +- GannetPreInitialise.m | 6 +- GannetSegment.m | 12 +- export_fig/SYNTAX | 217 ++++++++ export_fig/append_pdfs.m | 39 +- export_fig/crop_borders.m | 6 +- export_fig/eps2pdf.m | 43 +- export_fig/export_fig.m | 864 ++++++++++++++++++++++-------- export_fig/isolate_axes.m | 14 +- export_fig/print2array.m | 24 +- export_fig/print2eps.m | 16 + 14 files changed, 1031 insertions(+), 288 deletions(-) create mode 100644 export_fig/SYNTAX diff --git a/AlignSubSpectra.m b/AlignSubSpectra.m index 0087a8b..885f9a4 100644 --- a/AlignSubSpectra.m +++ b/AlignSubSpectra.m @@ -67,8 +67,6 @@ case 'GE' if strcmpi(MRS_struct.p.seqorig,'Lythgoe') subSpecInd = [3 2 4 1]; - elseif strcmpi(MRS_struct.p.seqorig,'Noeske') - subSpecInd = [2 1 3 4]; else subSpecInd = [3 2 1 4]; end diff --git a/CoRegStandAlone/CoRegStandAlone.m b/CoRegStandAlone/CoRegStandAlone.m index 2881df3..56deba8 100755 --- a/CoRegStandAlone/CoRegStandAlone.m +++ b/CoRegStandAlone/CoRegStandAlone.m @@ -43,8 +43,8 @@ % 1. Pre-initialise %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -MRS_struct.version.Gannet = '3.2.1-rc'; -MRS_struct.version.load = '220526'; +MRS_struct.version.Gannet = '3.2.1'; +MRS_struct.version.load = '220607'; MRS_struct.ii = 0; if size(metabfile,2) == 1 metabfile = metabfile'; @@ -53,7 +53,8 @@ MRS_struct.p.HERMES = 0; % Flags -MRS_struct.p.mat = 1; % Save results in *.mat output structure? (0 = NO, 1 = YES (default)). +MRS_struct.p.mat = 1; % Save results in *.mat file? (0 = NO, 1 = YES (default)). +MRS_struct.p.csv = 1; % Save results in *.csv file? (0 = NO, 1 = YES (default)). MRS_struct.p.vox = {'vox1'}; % Name of the voxel MRS_struct.p.target = {'GABAGlx'}; % Name of the target metabolite @@ -69,37 +70,37 @@ %%%%%%%%%%%%%%%%%%%%%%%%%%%%% for ii = 1:length(metabfile) % Loop over all files in the batch (from metabfile) - + MRS_struct.ii = ii; - + switch MRS_struct.p.vendor - + case 'GE' MRS_struct = GERead(MRS_struct, metabfile{ii}); - + case 'Siemens_twix' MRS_struct = SiemensTwixRead(MRS_struct, metabfile{ii}); - + case 'Siemens_dicom' MRS_struct = SiemensDICOMRead(MRS_struct, metabfile{ii}); - + case 'dicom' MRS_struct = DICOMRead(MRS_struct, metabfile{ii}); - + case 'Siemens_rda' MRS_struct = SiemensRead(MRS_struct, metabfile{ii}, metabfile{ii}); - + case 'Philips' MRS_struct = PhilipsRead(MRS_struct, metabfile{ii}); - + case 'Philips_data' MRS_struct = PhilipsRead_data(MRS_struct, metabfile{ii}); - + case 'Philips_raw' MRS_struct = PhilipsRawLoad(MRS_struct, metabfile{ii}, 3, 0); - + end - + end %%%%%%%%%%%%%%%%%%%%%%%%%%%%% @@ -124,6 +125,35 @@ save(fullfile(pwd, 'MRS_struct_CoRegStandAlone.mat'), 'MRS_struct', '-v7.3'); end +% Export MRS_struct fields into csv file +if MRS_struct.p.csv + csv_name = fullfile(pwd, 'MRS_struct.csv'); + if exist(csv_name, 'file') + fprintf('\nUpdating results in %s\n\n', 'MRS_struct.csv...'); + else + fprintf('\nExporting results to %s\n\n', 'MRS_struct.csv...'); + end + + if strcmp(MRS_struct.p.vendor, 'Siemens_rda') + filename = MRS_struct.metabfile(:,1:2:end)'; + else + filename = MRS_struct.metabfile'; + end + for ii = 1:length(filename) + [~,b,c] = fileparts(filename{ii}); + out.filename(ii,1) = cellstr([b c]); + end + + out.fGM = MRS_struct.out.vox1.tissue.fGM(:); + out.fWM = MRS_struct.out.vox1.tissue.fWM(:); + out.fCSF = MRS_struct.out.vox1.tissue.fCSF(:); + + round2 = @(x) round(x*1e3)/1e3; + T = table(out.filename, round2(out.fGM), round2(out.fWM), round2(out.fCSF), ... + 'VariableNames', {'Filename', 'fGM', 'fWM', 'fCSF'}); + writetable(T, csv_name); +end + end diff --git a/CoRegStandAlone/Seg.m b/CoRegStandAlone/Seg.m index 8ace991..21787a4 100755 --- a/CoRegStandAlone/Seg.m +++ b/CoRegStandAlone/Seg.m @@ -11,7 +11,7 @@ % This is useful if only the tissue segmentation information is supposed to % be obtained. -MRS_struct.version.segment = '210331'; +MRS_struct.version.segment = '220607'; vox = MRS_struct.p.vox(1); warning('off'); % temporarily suppress warning messages @@ -94,18 +94,18 @@ airvol_thresh = airvol_tmp .* T1_tmp; airvol_thresh = airvol_thresh(:); - MRS_struct.out.tissue.CV_WM(ii) = std(WMvol_thresh, 'omitnan') / mean(WMvol_thresh, 'omitnan'); - MRS_struct.out.tissue.CV_GM(ii) = std(GMvol_thresh, 'omitnan') / mean(GMvol_thresh, 'omitnan'); - MRS_struct.out.tissue.CJV(ii) = (std(WMvol_thresh, 'omitnan') + std(GMvol_thresh, 'omitnan')) ... + MRS_struct.out.QA.CV_WM(ii) = std(WMvol_thresh, 'omitnan') / mean(WMvol_thresh, 'omitnan'); + MRS_struct.out.QA.CV_GM(ii) = std(GMvol_thresh, 'omitnan') / mean(GMvol_thresh, 'omitnan'); + MRS_struct.out.QA.CJV(ii) = (std(WMvol_thresh, 'omitnan') + std(GMvol_thresh, 'omitnan')) ... / abs(mean(WMvol_thresh, 'omitnan') - mean(GMvol_thresh, 'omitnan')); - MRS_struct.out.tissue.CNR(ii) = abs(mean(WMvol_thresh, 'omitnan') - mean(GMvol_thresh, 'omitnan')) / ... + MRS_struct.out.QA.CNR(ii) = abs(mean(WMvol_thresh, 'omitnan') - mean(GMvol_thresh, 'omitnan')) / ... sqrt(var(airvol_thresh, 'omitnan') + var(WMvol_thresh, 'omitnan') + var(GMvol_thresh, 'omitnan')); T1_tmp = T1_tmp(:); n_vox = numel(T1_tmp); efc_max = n_vox * (1/sqrt(n_vox)) * log(1/sqrt(n_vox)); b_max = sqrt(sum(T1_tmp.^2)); - MRS_struct.out.tissue.EFC(ii) = (1/efc_max) .* sum((T1_tmp ./ b_max) .* log((T1_tmp + eps) ./ b_max)); + MRS_struct.out.QA.EFC(ii) = (1/efc_max) .* sum((T1_tmp ./ b_max) .* log((T1_tmp + eps) ./ b_max)); % Loop over voxels if PRIAM for kk = 1:length(vox) diff --git a/GannetLoad.m b/GannetLoad.m index 0ad02c1..165b281 100644 --- a/GannetLoad.m +++ b/GannetLoad.m @@ -13,8 +13,8 @@ % 6. Build GannetLoad output %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% -MRS_struct.version.Gannet = '3.2.1-rc'; -MRS_struct.version.load = '220526'; +MRS_struct.version.Gannet = '3.2.1'; +MRS_struct.version.load = '220607'; VersionCheck(0, MRS_struct.version.Gannet); ToolboxCheck; diff --git a/GannetPreInitialise.m b/GannetPreInitialise.m index 6fc4f01..f3e733f 100644 --- a/GannetPreInitialise.m +++ b/GannetPreInitialise.m @@ -12,7 +12,7 @@ % and MEGA-PRESS: {'GABA'}, {'Glx'}, {'GSH'}, {'Lac'}, or {'EtOH'} % and HERMES: {'GABA','GSH'}, {'Glx','GSH'}, {'Lac','GSH'}, or {'EtOH','GABA','GSH'} MRS_struct.p.seqorig = 'JHU'; % Origin of Philips MEGA-PRESS or GE HERMES sequences; - % options are 'JHU' or 'Philips' if Philips, or 'Lythgoe' or 'Noeske' if GE (HERMES only) + % options are 'JHU' or 'Philips' if Philips, or 'Lythgoe' if GE (HERMES only) % Analysis parameters MRS_struct.p.LB = 3; % Exponential line-broadening (in Hz) @@ -38,9 +38,9 @@ MRS_struct.p.mat = 0; % Save MRS_struct as a .mat file MRS_struct.p.csv = 0; % Extract useful data from MRS_struct and export them to a .csv file (applies to GannetFit, % GannetSegment and GannetQuantify) - MRS_struct.p.append = 1; % Append PDF outputs into one PDF (separately for each module) (requires export_fig in the Gannet + MRS_struct.p.append = 0; % Append PDF outputs into one PDF (separately for each module) (requires export_fig in the Gannet % folder to be added to the search path and GhostScript to be installed) - MRS_struct.p.hide = 0; % Do not dynamically display output figures + MRS_struct.p.hide = 0; % Do not display output figures end diff --git a/GannetSegment.m b/GannetSegment.m index 2295a53..0a5ae0c 100644 --- a/GannetSegment.m +++ b/GannetSegment.m @@ -7,7 +7,7 @@ % for the GM, WM and CSF segmentations. If these files are present, they % are loaded and used for the voxel segmentation -MRS_struct.version.segment = '210331'; +MRS_struct.version.segment = '220607'; warning('off'); % temporarily suppress warning messages @@ -110,18 +110,18 @@ airvol_thresh = airvol_tmp .* T1_tmp; airvol_thresh = airvol_thresh(:); - MRS_struct.out.tissue.CV_WM(ii) = std(WMvol_thresh, 'omitnan') / mean(WMvol_thresh, 'omitnan'); - MRS_struct.out.tissue.CV_GM(ii) = std(GMvol_thresh, 'omitnan') / mean(GMvol_thresh, 'omitnan'); - MRS_struct.out.tissue.CJV(ii) = (std(WMvol_thresh, 'omitnan') + std(GMvol_thresh, 'omitnan')) ... + MRS_struct.out.QA.CV_WM(ii) = std(WMvol_thresh, 'omitnan') / mean(WMvol_thresh, 'omitnan'); + MRS_struct.out.QA.CV_GM(ii) = std(GMvol_thresh, 'omitnan') / mean(GMvol_thresh, 'omitnan'); + MRS_struct.out.QA.CJV(ii) = (std(WMvol_thresh, 'omitnan') + std(GMvol_thresh, 'omitnan')) ... / abs(mean(WMvol_thresh, 'omitnan') - mean(GMvol_thresh, 'omitnan')); - MRS_struct.out.tissue.CNR(ii) = abs(mean(WMvol_thresh, 'omitnan') - mean(GMvol_thresh, 'omitnan')) / ... + MRS_struct.out.QA.CNR(ii) = abs(mean(WMvol_thresh, 'omitnan') - mean(GMvol_thresh, 'omitnan')) / ... sqrt(var(airvol_thresh, 'omitnan') + var(WMvol_thresh, 'omitnan') + var(GMvol_thresh, 'omitnan')); T1_tmp = T1_tmp(:); n_vox = numel(T1_tmp); efc_max = n_vox * (1/sqrt(n_vox)) * log(1/sqrt(n_vox)); b_max = sqrt(sum(T1_tmp.^2)); - MRS_struct.out.tissue.EFC(ii) = (1/efc_max) .* sum((T1_tmp ./ b_max) .* log((T1_tmp + eps) ./ b_max)); + MRS_struct.out.QA.EFC(ii) = (1/efc_max) .* sum((T1_tmp ./ b_max) .* log((T1_tmp + eps) ./ b_max)); % Voxel mask voxmaskvol = spm_vol(MRS_struct.mask.(vox{kk}).outfile{ii}); diff --git a/export_fig/SYNTAX b/export_fig/SYNTAX new file mode 100644 index 0000000..1d9a534 --- /dev/null +++ b/export_fig/SYNTAX @@ -0,0 +1,217 @@ +Syntax: + [imageData, alpha] = export_fig(filename, [handle], options...) + +Examples: + imageData = export_fig + [imageData, alpha] = export_fig + export_fig filename + export_fig filename -format1 -format2 + export_fig ... -nocrop + export_fig ... -c[,,,] + export_fig ... -transparent + export_fig ... -native + export_fig ... -m + export_fig ... -r + export_fig ... -a + export_fig ... -q + export_fig ... -p + export_fig ... -d + export_fig ... -depsc + export_fig ... - + export_fig ... - + export_fig ... -append + export_fig ... -bookmark + export_fig ... -clipboard<:format> + export_fig ... -update + export_fig ... -version + export_fig ... -nofontswap + export_fig ... -font_space + export_fig ... -linecaps + export_fig ... -noinvert + export_fig ... -preserve_size + export_fig ... -options + export_fig ... -silent + export_fig ... -regexprep + export_fig ... -toolbar + export_fig ... -menubar + export_fig(..., handle) + export_fig(..., figName) + +Description: + This function saves a figure or single axes to one or more vector and/or + bitmap file formats, and/or outputs a rasterized version to the workspace, + with the following properties: + - Figure/axes reproduced as it appears on screen + - Cropped/padded borders (optional) + - Embedded fonts (vector formats) + - Improved line and grid line styles + - Anti-aliased graphics (bitmap formats) + - Render images at native resolution (optional for bitmap formats) + - Transparent background supported (pdf, eps, png, tif, gif) + - Semi-transparent patch objects supported (png, tif) + - RGB, CMYK or grayscale output (CMYK only with pdf, eps, tif) + - Variable image compression, including lossless (pdf, eps, jpg) + - Optional rounded line-caps (pdf, eps) + - Optionally append to file (pdf, tif, gif) + - Vector formats: pdf, eps, emf, svg + - Bitmap formats: png, tif, jpg, bmp, gif, clipboard, export to workspace + + This function is especially suited to exporting figures for use in + publications and presentations, because of the high quality and + portability of media produced. + + Note that the background color and figure dimensions are reproduced + (the latter approximately, and ignoring cropping & magnification) in the + output file. For transparent background (and semi-transparent patch + objects), use the -transparent option or set the figure 'Color' property + to 'none'. To make axes transparent set the axes 'Color' property to + 'none'. PDF, EPS, TIF & PNG are the only formats that support a transparent + background; only TIF & PNG formats support transparency of patch objects. + + The choice of renderer (opengl/zbuffer/painters) has a large impact on the + output quality. The default value (opengl for bitmaps, painters for vector + formats) generally gives good results, but if you aren't satisfied + then try another renderer. Notes: + 1) For vector formats (EPS,PDF), only painters generates vector graphics + 2) For bitmap formats, only opengl correctly renders transparent patches + 3) For bitmap formats, only painters correctly scales line dash and dot + lengths when magnifying or anti-aliasing + 4) Fonts may be substitued with Courier when using painters + + When exporting to vector format (PDF & EPS) and bitmap format using the + painters renderer, this function requires that ghostscript is installed + on your system. You can download this from: http://www.ghostscript.com + When exporting to EPS it additionally requires pdftops, from the Xpdf + suite of functions. You can download this from: http://xpdfreader.com + + SVG output uses Matlab's built-in SVG export if available, or otherwise the + fig2svg (https://github.com/kupiqu/fig2svg) or plot2svg + (https://github.com/jschwizer99/plot2svg) utilities, if available. + Note: cropping/padding are not supported in export_fig's SVG and EMF output. + +Inputs: + filename - string containing the name (optionally including full or + relative path) of the file the figure is to be saved as. If + a path is not specified, the figure is saved in the current + directory. If no name and no output arguments are specified, + the default name, 'export_fig_out', is used. If neither a + file extension nor a format are specified, a ".png" is added + and the figure saved in that format. + - - string(s) containing the output file extension(s). Options: + '-pdf', '-eps', 'emf', '-svg', '-png', '-tif', '-jpg' and '-bmp'. + Multiple formats can be specified, without restriction. + For example: export_fig('-jpg', '-pdf', '-png', ...) + Note: '-tif','-tiff' are equivalent, and so are '-jpg','-jpeg'. + -transparent - option indicating that the figure background is to be made + transparent (PNG,PDF,TIF,EPS,EMF formats only). Implies -noinvert. + -nocrop - option indicating that empty margins should not be cropped. + -c[,,,] - option indicating crop amounts. Must be + a 4-element vector of numeric values: [top,right,bottom,left] + where NaN/Inf indicates auto-cropping, 0 means no cropping, any + other value means cropping in pixel amounts. e.g. '-c7,15,0,NaN' + Note: this option is not supported by SVG and EMF formats. + -p - option to pad a border of width val to exported files, where + val is either a relative size with respect to cropped image + size (i.e. p=0.01 adds a 1border). For EPS & PDF formats, + val can also be integer in units of 1/72" points (abs(val)>1). + val can be positive (padding) or negative (extra cropping). + If used, the -nocrop flag will be ignored, i.e. the image will + always be cropped and then padded. Default: 0 (i.e. no padding). + Note: this option is not supported by SVG and EMF formats. + -m - option val indicates the factor to magnify the figure dimensions + when generating bitmap outputs (does not affect vector formats). + Default: '-m1' (i.e. val=1). Note: val~=1 slows down export_fig. + -r - option val indicates the resolution (in pixels per inch) to + export bitmap and vector outputs, without changing dimensions of + the on-screen figure. Default: '-r864' (for vector output only). + Note: -m option overides -r option for bitmap exports only. + -native - option indicating that the output resolution (when outputting + a bitmap format) should be such that the vertical resolution + of the first suitable image found in the figure is at the + native resolution of that image. To specify a particular + image to use, give it the tag 'export_fig_native'. + Notes: This overrides any value set with the -m and -r options. + It also assumes that the image is displayed front-to-parallel + with the screen. The output resolution is approximate and + should not be relied upon. Anti-aliasing can have adverse + effects on image quality (disable with the -a1 option). + -a1, -a2, -a3, -a4 - option indicating the amount of anti-aliasing (AA) to + use for bitmap outputs, when GraphicsSmoothing is not available. + '-a1'=no AA; '-a4'=max. Default: 3 for HG1, 1 for HG2. + - - option to force a particular renderer (painters, opengl or + [in R2014a or older] zbuffer). Default value: opengl for bitmap + formats or figures with patches and/or transparent annotations; + painters for vector formats without patches/transparencies. + - - option indicating which colorspace color figures should + be saved in: RGB (default), CMYK or gray. Usage example: '-gray'. + Note: CMYK is only supported in PDF, EPS and TIF formats. + -q - option to vary bitmap image quality (PDF, EPS, JPG formats only). + A larger val, in the range 0-100, produces higher quality and + lower compression. val > 100 results in lossless compression. + Default: '-q95' for JPG, ghostscript prepress default for PDF,EPS. + Note: lossless compression can sometimes give a smaller file size + than the default lossy compression, depending on the image type. + -append - option indicating that if the file already exists the figure is to + be appended as a new page, instead of being overwritten (default). + PDF, TIF & GIF output formats only (multi-image GIF = animated). + -bookmark - option to indicate that a bookmark with the name of the + figure is to be created in the output file (PDF format only). + -clipboard - option to save output as an image on the system clipboard. + -clipboard<:format> - copies to clipboard in the specified format: + image (default), bitmap, emf, or pdf. + Notes: Only -clipboard (or -clipboard:image, which is the same) + applies export_fig parameters such as cropping, padding etc. + Only the emf format supports -transparent background + -clipboard:image create a bitmap image using export_fig processing + -clipboard:bitmap create a bitmap image as-is (no auto-cropping etc.) + -clipboard:emf is vector format without auto-cropping; Windows-only + -clipboard:pdf is vector format without cropping; not universally supported + -d - option to indicate a ghostscript setting. For example, + -dMaxBitmap=0 or -dNoOutputFonts (Ghostscript 9.15+). + -depsc - option to use EPS level-3 rather than the default level-2 print + device. This solves some bugs with Matlab's default -depsc2 device + such as discolored subplot lines on images (vector formats only). + -update - option to download and install the latest version of export_fig + -version - return the current export_fig version, without any figure export + -nofontswap - option to avoid font swapping. Font swapping is automatically + done in vector formats (only): 11 standard Matlab fonts are + replaced by the original figure fonts. This option prevents this. + -font_space - option to set a spacer character for font-names that + contain spaces, used by EPS/PDF. Default: '' + -linecaps - option to create rounded line-caps (vector formats only). + -noinvert - option to avoid setting figure's InvertHardcopy property to + 'off' during output (this solves some problems of empty outputs). + -preserve_size - option to preserve the figure's PaperSize property in output + file (PDF/EPS formats only; default is to not preserve it). + -options - format-specific parameters as defined in Matlab's + documentation of the imwrite function, contained in a struct under + the format name. For example to specify the JPG Comment parameter, + pass a struct such as this: options.JPG.Comment='abc'. Similarly, + options.PNG.BitDepth=4. Only used by PNG,TIF,JPG,GIF output formats. + Options can also be specified as a cell array of name-value pairs, + e.g. {'BitDepth',4, 'Author','Yair'} - these options will be used + by all supported output formats of the export_fig command. + -silent - option to avoid various warning and informational messages, such + as version update checks, transparency or renderer issues, etc. + -regexprep - replaces all occurances of (a regular expression + string or array of strings; case-sensitive), with the corresponding + string(s), in EPS/PDF files (only). See regexp function's doc. + Warning: invalid replacement can make your EPS/PDF file unreadable! + -toolbar - adds an interactive export button to the figure's toolbar + -menubar - adds an interactive export menu to the figure's menubar + handle - handle of the figure, axes or uipanels (can be an array of handles + but all the objects must be in the same figure) to be exported. + Default: gcf (handle of current figure). + figName - name (title) of the figure to export (e.g. 'Figure 1' or 'My fig'). + Overriden by handle (if specified); Default: current figure + +Outputs: + imageData - MxNxC uint8 image array of the exported image. + alpha - MxN single array of alphamatte values in the range [0,1], + for the case when the background is transparent. + +Some helpful examples/tips are listed at: https://github.com/altmany/export_fig + +See also PRINT, SAVEAS, ScreenCapture (on the Matlab File Exchange) + +Copyright (C) Oliver Woodford 2008-2014, Yair Altman 2015- diff --git a/export_fig/append_pdfs.m b/export_fig/append_pdfs.m index d8bd376..efd82ae 100644 --- a/export_fig/append_pdfs.m +++ b/export_fig/append_pdfs.m @@ -1,7 +1,7 @@ function append_pdfs(varargin) %APPEND_PDFS Appends/concatenates multiple PDF files % -% Example: +% Usage example: % append_pdfs(outputFilename, inputFilename1, inputFilename2, ...) % append_pdfs(outputFilename, inputFilenames_list{:}) % append_pdfs(outputFilename, inputFilenames_cell_or_string_array) @@ -13,10 +13,10 @@ function append_pdfs(varargin) % This function requires that you have ghostscript installed on your % system. Ghostscript can be downloaded from: http://www.ghostscript.com % -% IN: -% output - string of output file name (including the extension, .pdf). +% Inputs: +% output - output file name (including the .pdf extension). % If it exists it is appended to; if not, it is created. -% input1 - string of an input file name (including the extension, .pdf). +% input1 - input file name(s) (including the .pdf extension). % All input files are appended in order. % input_list - cell array list of input file name strings. All input % files are appended in order. @@ -40,6 +40,7 @@ function append_pdfs(varargin) % 06/12/18: Avoid an "invalid escape-char" warning upon error % 22/03/20: Alert if ghostscript.m is not found on Matlab path % 29/03/20: Accept a cell-array of input files (issue #299); accept both "strings", 'chars' +% 25/01/22: Improved handling of missing input files & folder with non-ASCII chars (issue #349) %} if nargin < 2, return; end % sanity check @@ -52,16 +53,27 @@ function append_pdfs(varargin) varargin = {varargin{1} varargin{2}{:}}; %#ok end + % Handle special cases of input args + numArgs = numel(varargin); + if numArgs < 2 + error('export_fig:append_pdfs:NoInputs', 'append_pdfs: Missing input filenames') + end + % Ensure that ghostscript() exists on the Matlab path if ~exist('ghostscript','file') error('export_fig:append_pdfs:ghostscript', 'The ghostscript.m function is required by append_pdf.m. Install the complete export_fig package from https://www.mathworks.com/matlabcentral/fileexchange/23629-export_fig or https://github.com/altmany/export_fig') end - % Are we appending or creating a new file + % Are we appending or creating a new file? append = exist(varargin{1}, 'file') == 2; + if ~append && numArgs == 2 % only 1 input file - copy it directly to output + copyfile(varargin{2}, varargin{1}); + return + end + + % Ensure that the temp dir is writable (Javier Paredes 26/2/15) output = [tempname '.pdf']; try - % Ensure that the temp dir is writable (Javier Paredes 26/2/15) fid = fopen(output,'w'); fwrite(fid,1); fclose(fid); @@ -79,6 +91,14 @@ function append_pdfs(varargin) varargin = varargin(2:end); end + % Ensure that all input files exist + for fileIdx = 2 : numel(varargin) + filename = varargin{fileIdx}; + if ~exist(filename,'file') + error('export_fig:append_pdf:MissingFile','Input file %s does not exist',filename); + end + end + % Create the command file if isTempDirOk cmdfile = [tempname '.txt']; @@ -108,7 +128,7 @@ function append_pdfs(varargin) % Check for ghostscript execution errors if status errMsg = strrep(errMsg,'\','\\'); % Avoid an "invalid escape-char" warning - error('YMA:export_fig:append_pdf',errMsg); + error('export_fig:append_pdf:ghostscriptError',errMsg); end % Rename the file if needed @@ -130,8 +150,9 @@ function prepareCmdFile(cmdfile, output, varargin) [fpath,fname,fext] = fileparts(pathStr); if isempty(fpath) || strcmpi(fpath,pathStr), return, end dirOutput = evalc(['system(''dir /X /AD "' pathStr '*"'')']); - shortName = strtrim(regexprep(dirOutput,{'.*> *',[fname fext '.*']},'')); - if isempty(shortName) + regexpStr = ['.*\s(\S+)\s*' fname fext '.*']; + shortName = regexprep(dirOutput,regexpStr,'$1'); + if isempty(shortName) || isequal(shortName,dirOutput) shortName = [fname fext]; end fpath = normalizePath(fpath); %recursive until entire fpath is processed diff --git a/export_fig/crop_borders.m b/export_fig/crop_borders.m index b87c842..83abd61 100644 --- a/export_fig/crop_borders.m +++ b/export_fig/crop_borders.m @@ -24,6 +24,7 @@ % 21/02/16: Enabled specifying non-automated crop amounts % 04/04/16: Fix per Luiz Carvalho for old Matlab releases % 23/10/16: Fixed issue #175: there used to be a 1px minimal padding in case of crop, now removed +% 15/05/22: Fixed EPS bounding box (issue #356) %} if nargin < 3 @@ -149,7 +150,10 @@ end % For EPS cropping, determine the relative BoundingBox - bb_rel - bb_rel = [l-1 h-b-1 r+1 h-t+1]./[w h w h]; + bb_pixels = [l-1 h-b-1 r+1 h-t+1]; %[LowerLeftXY, UpperRightXY] + bb_pixels(bb_pixels<0) = 0; + bb_pixels = min(bb_pixels, [w h w h]); + bb_rel = bb_pixels ./ [w h w h]; end function A = col(A) diff --git a/export_fig/eps2pdf.m b/export_fig/eps2pdf.m index eb839f2..aef0c08 100644 --- a/export_fig/eps2pdf.m +++ b/export_fig/eps2pdf.m @@ -15,8 +15,8 @@ function eps2pdf(source, dest, crop, append, gray, quality, gs_options) % page on the end of the eps file. The level of bitmap compression can also % optionally be set. % -% This function requires that you have ghostscript installed on your -% system. Ghostscript can be downloaded from: http://www.ghostscript.com +% This function requires that you have ghostscript installed on your system. +% Ghostscript can be downloaded from: http://www.ghostscript.com % % Inputs: % source - filename of the source eps file to convert. The filename is @@ -59,6 +59,7 @@ function eps2pdf(source, dest, crop, append, gray, quality, gs_options) % 15/01/20: Added information about the GS/destination filepath in case of error (issue #294) % 20/01/20: Attempted fix for issue #285: unsupported patch transparency in some Ghostscript versions % 12/02/20: Improved fix for issue #285: add -dNOSAFER and -dALLOWPSTRANSPARENCY (thanks @linasstonys) +% 26/08/21: Added GS version to error message; fixed some problems with PDF append (issue #339) % Intialise the options string for ghostscript options = ['-q -dNOPAUSE -dBATCH -sDEVICE=pdfwrite -dPDFSETTINGS=/prepress -sOutputFile="' dest '"']; @@ -110,6 +111,13 @@ function eps2pdf(source, dest, crop, append, gray, quality, gs_options) % Check if the output file exists if nargin > 3 && append && exist(dest, 'file') == 2 + % Store the original filesize for later use below + try + file_info = dir(dest); + orig_bytes = file_info.bytes; + catch + orig_bytes = []; + end % File exists - append current figure to the end tmp_nam = [tempname '.pdf']; [fpath,fname,fext] = fileparts(tmp_nam); @@ -124,28 +132,45 @@ function eps2pdf(source, dest, crop, append, gray, quality, gs_options) fpath = fileparts(dest); tmp_nam = fullfile(fpath,[fname fext]); end - % Copy the existing (dest) pdf file to temporary folder + % Copy the original (dest) pdf file to temporary folder copyfile(dest, tmp_nam); % Produce an interim pdf of the source eps, rather than adding the eps directly (issue #233) + % this will override the original (dest) pdf file + orig_options = options; ghostscript([options ' -f "' source '"']); [~,fname] = fileparts(tempname); tmp_nam2 = fullfile(fpath,[fname fext]); % ensure using a writable folder (not necessarily tempdir) copyfile(dest, tmp_nam2); - % Add the existing pdf and interim pdf as inputs to ghostscript + % Add the original pdf (tmp_nam) and interim pdf (dest=>tmp_nam2) as inputs to ghostscript %options = [options ' -f "' tmp_nam '" "' source '"']; % append the source eps to dest pdf options = [options ' -f "' tmp_nam '" "' tmp_nam2 '"']; % append the interim pdf to dest pdf try % Convert to pdf using ghostscript [status, message] = ghostscript(options); + % The output pdf should now be in dest + + % If the returned message is non-empty, a possible error may have + % occured, so check the file size to ensure whether the file grew + if ~isempty(message) && ~isempty(orig_bytes) + file_info = dir(dest); + new_bytes = file_info.bytes; + if new_bytes < orig_bytes + 100 + % Looks like nothing substantial (if anything) was appended to + % the original pdf, so try adding the eps file directly (issue #339) + options = [orig_options ' -f "' tmp_nam '" "' source '"']; % append the source eps to dest pdf + [status, message] = ghostscript(options); + end + end + + % Delete the intermediate (temporary) files + delete(tmp_nam); + delete(tmp_nam2); catch me % Delete the intermediate files and rethrow the error delete(tmp_nam); delete(tmp_nam2); rethrow(me); end - % Delete the intermediate (temporary) files - delete(tmp_nam); - delete(tmp_nam2); else % File doesn't exist or should be over-written % Add the source eps file as input to ghostscript @@ -222,8 +247,10 @@ function eps2pdf(source, dest, crop, append, gray, quality, gs_options) fprintf(2,'%s',msg); end fprintf(2, '\n * perhaps %s is open by another application\n', dest); + try gs_version = str2num(evalc('ghostscript(''--version'');')); catch, gs_version = ''; end %#ok + if ~isempty(gs_version), gs_version = [' ' num2str(gs_version)]; end if ~isempty(gs_options) - fprintf(2, ' * or maybe your Ghostscript version does not accept the extra "%s" option(s) that you requested\n', gs_options); + fprintf(2, ' * or maybe your Ghostscript version%s does not accept the extra "%s" option(s) that you requested\n', gs_version, gs_options); end fprintf(2, ' * or maybe you have another gs executable in your system''s path\n\n'); fprintf(2, 'Ghostscript path: %s\n', user_string('ghostscript')); diff --git a/export_fig/export_fig.m b/export_fig/export_fig.m index e400756..f29492d 100644 --- a/export_fig/export_fig.m +++ b/export_fig/export_fig.m @@ -32,7 +32,10 @@ % export_fig ... -options % export_fig ... -silent % export_fig ... -regexprep +% export_fig ... -toolbar +% export_fig ... -menubar % export_fig(..., handle) +% export_fig(..., figName) % % This function saves a figure or single axes to one or more vector and/or % bitmap file formats, and/or outputs a rasterized version to the workspace, @@ -43,14 +46,14 @@ % - Improved line and grid line styles % - Anti-aliased graphics (bitmap formats) % - Render images at native resolution (optional for bitmap formats) -% - Transparent background supported (pdf, eps, png, tif) +% - Transparent background supported (pdf, eps, png, tif, gif) % - Semi-transparent patch objects supported (png, tif) % - RGB, CMYK or grayscale output (CMYK only with pdf, eps, tif) % - Variable image compression, including lossless (pdf, eps, jpg) % - Optional rounded line-caps (pdf, eps) -% - Optionally append to file (pdf, tif) +% - Optionally append to file (pdf, tif, gif) % - Vector formats: pdf, eps, emf, svg -% - Bitmap formats: png, tif, jpg, bmp, clipboard, export to workspace +% - Bitmap formats: png, tif, jpg, bmp, gif, clipboard, export to workspace % % This function is especially suited to exporting figures for use in % publications and presentations, because of the high quality and @@ -80,9 +83,9 @@ % When exporting to EPS it additionally requires pdftops, from the Xpdf % suite of functions. You can download this from: http://xpdfreader.com % -% SVG output uses the fig2svg (https://github.com/kupiqu/fig2svg) or plot2svg -% (https://github.com/jschwizer99/plot2svg) utilities, or Matlab's built-in -% SVG export if neither of these utilities are available on Matlab's path. +% SVG output uses Matlab's built-in SVG export if available, or otherwise the +% fig2svg (https://github.com/kupiqu/fig2svg) or plot2svg +% (https://github.com/jschwizer99/plot2svg) utilities, if available. % Note: cropping/padding are not supported in export_fig's SVG and EMF output. % % Inputs: @@ -149,7 +152,7 @@ % than the default lossy compression, depending on the image type. % -append - option indicating that if the file already exists the figure is to % be appended as a new page, instead of being overwritten (default). -% PDF & TIF output formats only. +% PDF, TIF & GIF output formats only (multi-image GIF = animated). % -bookmark - option to indicate that a bookmark with the name of the % figure is to be created in the output file (PDF format only). % -clipboard - option to save output as an image on the system clipboard. @@ -183,16 +186,23 @@ % documentation of the imwrite function, contained in a struct under % the format name. For example to specify the JPG Comment parameter, % pass a struct such as this: options.JPG.Comment='abc'. Similarly, -% options.PNG.BitDepth=4. Valid only for PNG,TIF,JPG output formats. +% options.PNG.BitDepth=4. Only used by PNG,TIF,JPG,GIF output formats. +% Options can also be specified as a cell array of name-value pairs, +% e.g. {'BitDepth',4, 'Author','Yair'} - these options will be used +% by all supported output formats of the export_fig command. % -silent - option to avoid various warning and informational messages, such % as version update checks, transparency or renderer issues, etc. % -regexprep - replaces all occurances of (a regular expression % string or array of strings; case-sensitive), with the corresponding % string(s), in EPS/PDF files (only). See regexp function's doc. % Warning: invalid replacement can make your EPS/PDF file unreadable! -% handle - The handle of the figure, axes or uipanels (can be an array of -% handles, but the objects must be in the same figure) which is -% to be saved. Default: gcf (handle of current figure). +% -toolbar - adds an interactive export button to the figure's toolbar +% -menubar - adds an interactive export menu to the figure's menubar +% handle - handle of the figure, axes or uipanels (can be an array of handles +% but all the objects must be in the same figure) to be exported. +% Default: gcf (handle of current figure). +% figName - name (title) of the figure to export (e.g. 'Figure 1' or 'My fig'). +% Overriden by handle (if specified); Default: current figure % % Outputs: % imageData - MxNxC uint8 image array of the exported image. @@ -324,6 +334,18 @@ % 07/10/20: (3.13) Added version info and change-log links to update message (issue #322); Added -version option to return the current export_fig version; Avoid JavaFrame warning message; Improved exportgraphics/copygraphics infomercial message inc. support of upcoming Matlab R2021a % 10/12/20: (3.14) Enabled user-specified regexp replacements in generated EPS/PDF files (issue #324) % 01/07/21: (3.15) Added informative message in case of setopacityalpha error (issue #285) +% 26/08/21: (3.16) Fixed problem of white elements appearing transparent (issue #330); clarified some error messages +% 27/09/21: (3.17) Made Matlab's builtin export the default for SVG, rather than fig2svg/plot2svg (issue #316); updated transparency error message (issues #285, #343); reduced promo message frequency +% 03/10/21: (3.18) Fixed warning about invalid escaped character when the output folder does not exist (issue #345) +% 25/10/21: (3.19) Fixed print error when exporting a specific subplot (issue #347); avoid duplicate error messages +% 11/12/21: (3.20) Added GIF support, including animated & transparent-background; accept format options as cell-array, not just nested struct +% 20/12/21: (3.21) Speedups; fixed exporting non-current figure (hopefully fixes issue #318); fixed warning when appending to animated GIF +% 02/03/22: (3.22) Fixed small potential memory leak during screen-capture; expanded exportgraphics message for vector exports; fixed rotated tick labels on R2021a+ +% 02/03/22: (3.23) Added -toolbar and -menubar options to add figure toolbar/menubar items for interactive figure export (issue #73); fixed edge-case bug with GIF export +% 14/03/22: (3.24) Added support for specifying figure name in addition to handle; added warning when trying to export TIF/JPG/BMP with transparency; use current figure as default handle even when its HandleVisibility is not 'on' +% 16/03/22: (3.25) Fixed occasional empty files due to excessive cropping (issues #318, #350, #351) +% 01/05/22: (3.26) Added -transparency option for TIFF files +% 15/05/22: (3.27) Fixed EPS bounding box (issue #356) %} if nargout @@ -333,33 +355,42 @@ % Ensure the figure is rendered correctly _now_ so that properties like axes limits are up-to-date drawnow; - pause(0.02); % this solves timing issues with Java Swing's EDT (http://undocumentedmatlab.com/blog/solving-a-matlab-hang-problem) + pause(0.05); % this solves timing issues with Java Swing's EDT (http://undocumentedmatlab.com/blog/solving-a-matlab-hang-problem) - % Display promo (just once a week!) - try promo_time = getpref('export_fig','promo_time'); catch, promo_time=-inf; end - if abs(now-promo_time) > 7 && ~isdeployed + % Display promo (just once every 10 days!) + persistent promo_time + if isempty(promo_time) + try promo_time = getpref('export_fig','promo_time'); catch, promo_time=-inf; end + end + if abs(now-promo_time) > 10 && ~isdeployed programsCrossCheck; msg = char('Gps!qspgfttjpobm!Nbumbc!bttjtubodf-!qmfbtf!dpoubdu!=%?'-1); url = char('iuuqt;00VoepdvnfoufeNbumbc/dpn0dpotvmujoh'-1); displayPromoMsg(msg, url); + promo_time = now; setpref('export_fig','promo_time',now) end - % Parse the input arguments + % Use the current figure as the default figure handle + % temporarily set ShowHiddenHandles='on' to access figure with HandleVisibility='off' + try oldValue = get(0,'ShowHiddenHandles'); set(0,'ShowHiddenHandles','on'); catch, end fig = get(0, 'CurrentFigure'); + try set(0,'ShowHiddenHandles',oldValue); catch, end + + % Parse the input arguments argNames = {}; for idx = nargin:-1:1, argNames{idx} = inputname(idx); end [fig, options] = parse_args(nargout, fig, argNames, varargin{:}); % Check for newer version and exportgraphics/copygraphics compatibility - currentVersion = 3.15; + currentVersion = 3.27; if options.version % export_fig's version requested - return it and bail out imageData = currentVersion; return end if ~options.silent % Check for newer version (not too often) - checkForNewerVersion(3.15); % ...(currentVersion) is better but breaks in version 3.05- due to regexp limitation in checkForNewerVersion() + checkForNewerVersion(currentVersion); % this breaks in version 3.05- due to regexp limitation in checkForNewerVersion() % Hint to users to use exportgraphics/copygraphics in certain cases alertForExportOrCopygraphics(options); @@ -374,8 +405,8 @@ else oldWarn = warning('off','MATLAB:HandleGraphics:ObsoletedProperty:JavaFrame'); warning off MATLAB:ui:javaframe:PropertyToBeRemoved - uifig = handle(ancestor(fig,'figure')); - try jf = get(uifig,'JavaFrame_I'); catch, try jf = get(uifig,'JavaFrame'); catch, jf=1; end, end %#ok + hFig = handle(ancestor(fig,'figure')); + try jf = get(hFig,'JavaFrame_I'); catch, try jf = get(hFig,'JavaFrame'); catch, jf=1; end, end %#ok warning(oldWarn); if isempty(jf) % this is a uifigure %error('export_fig:uifigures','Figures created using the uifigure command or App Designer are not supported by export_fig. See %s for details.', hyperlink('https://github.com/altmany/export_fig/issues/261','issue #261')); @@ -394,10 +425,10 @@ end try % Create an invisible legacy figure at the same position/size as the uifigure - hNewFig = figure('Units',uifig.Units, 'Position',uifig.Position, 'MenuBar','none', 'ToolBar','none', 'Visible','off'); + hNewFig = figure('Units',hFig.Units, 'Position',hFig.Position, 'MenuBar','none', 'ToolBar','none', 'Visible','off'); % Copy the uifigure contents onto the new invisible legacy figure try - hChildren = allchild(uifig); %=uifig.Children; + hChildren = allchild(hFig); %=uifig.Children; copyobj(hChildren,hNewFig); catch if ~options.silent @@ -431,6 +462,16 @@ end end + % If toolbar button was requested, add it to the specified figure(s) + if options.toolbar + addToolbarButton(hFig, options); + end + + % If menubar menu was requested, add it to the specified figure(s) + if options.menubar + addMenubarMenu(hFig, options); + end + % Isolate the subplot, if it is one cls = all(ismember(get(fig, 'Type'), {'axes', 'uipanel'})); if cls @@ -469,25 +510,29 @@ Hlims = findall(fig, 'Type', 'axes'); if ~cls % Record the old axes limit and tick modes - Xlims = make_cell(get(Hlims, 'XLimMode')); - Ylims = make_cell(get(Hlims, 'YLimMode')); - Zlims = make_cell(get(Hlims, 'ZLimMode')); - Xtick = make_cell(get(Hlims, 'XTickMode')); - Ytick = make_cell(get(Hlims, 'YTickMode')); - Ztick = make_cell(get(Hlims, 'ZTickMode')); - Xlabel = make_cell(get(Hlims, 'XTickLabelMode')); - Ylabel = make_cell(get(Hlims, 'YTickLabelMode')); - Zlabel = make_cell(get(Hlims, 'ZTickLabelMode')); + Xlims = make_cell(get(Hlims, 'XLimMode')); + Ylims = make_cell(get(Hlims, 'YLimMode')); + Zlims = make_cell(get(Hlims, 'ZLimMode')); + Xtick = make_cell(get(Hlims, 'XTickMode')); + Ytick = make_cell(get(Hlims, 'YTickMode')); + Ztick = make_cell(get(Hlims, 'ZTickMode')); + Xlabel = make_cell(get(Hlims, 'XTickLabelMode')); + Ylabel = make_cell(get(Hlims, 'YTickLabelMode')); + Zlabel = make_cell(get(Hlims, 'ZTickLabelMode')); + try % XTickLabelRotation etc. was added in R2021a + Xtkrot = make_cell(get(Hlims, 'XTickLabelRotationMode')); + Ytkrot = make_cell(get(Hlims, 'YTickLabelRotationMode')); + Ztkrot = make_cell(get(Hlims, 'ZTickLabelRotationMode')); + catch + end % only in R2021a+ end % Set all axes limit and tick modes to manual, so the limits and ticks can't change % Fix Matlab R2014b bug (issue #34): plot markers are not displayed when ZLimMode='manual' - set(Hlims, 'XLimMode', 'manual', 'YLimMode', 'manual'); - set_tick_mode(Hlims, 'X'); - set_tick_mode(Hlims, 'Y'); + set_manual_axes_modes(Hlims, 'X'); + set_manual_axes_modes(Hlims, 'Y'); if ~using_hg2(fig) - set(Hlims,'ZLimMode', 'manual'); - set_tick_mode(Hlims, 'Z'); + set_manual_axes_modes(Hlims, 'Z'); end catch % ignore - fix issue #4 (using HG2 on R2014a and earlier) @@ -568,101 +613,105 @@ % Print large version to array [A, tcol, alpha] = getFigImage(fig, magnify, renderer, options, pixelpos); % Get the background colour - if options.transparent && (options.png || options.alpha) - try %options.aa_factor < 4 % default, faster but lines are not anti-aliased - % If all pixels are indicated as opaque (i.e. something went wrong with the Java screen-capture) - isBgColor = A(:,:,1) == tcol(1) & ... - A(:,:,2) == tcol(2) & ... - A(:,:,3) == tcol(3); - % Set the bgcolor pixels to be fully-transparent - A(repmat(isBgColor,[1,1,3])) = 255; %=white % TODO: more memory efficient without repmat - alpha(isBgColor) = 0; - catch % older logic - much slower and causes figure flicker - if true % to fold the code below... - % Get out an alpha channel - % MATLAB "feature": black colorbar axes can change to white and vice versa! - hCB = findall(fig, 'Type','axes', 'Tag','Colorbar'); - if isempty(hCB) - yCol = []; - xCol = []; - else - yCol = get(hCB, 'YColor'); - xCol = get(hCB, 'XColor'); - if iscell(yCol) - yCol = cell2mat(yCol); - xCol = cell2mat(xCol); - end - yCol = sum(yCol, 2); - xCol = sum(xCol, 2); - end - % MATLAB "feature": apparently figure size can change when changing - % colour in -nodisplay mode - % Set the background colour to black, and set size in case it was - % changed internally - set(fig, 'Color', 'k', 'Position', pos); - % Correct the colorbar axes colours - set(hCB(yCol==0), 'YColor', [0 0 0]); - set(hCB(xCol==0), 'XColor', [0 0 0]); - % Correct black axes color to off-black (issue #249) - hAxes = findall(fig, 'Type','axes'); - [hXs,hXrs] = fixBlackAxle(hAxes, 'XColor'); - [hYs,hYrs] = fixBlackAxle(hAxes, 'YColor'); - [hZs,hZrs] = fixBlackAxle(hAxes, 'ZColor'); - - % The following code might cause out-of-memory errors - try - % Print large version to array - B = print2array(fig, magnify, renderer); - % Downscale the image - B = downsize(single(B), options.aa_factor); - catch - % This is more conservative in memory, but kills transparency (issue #58) - B = single(print2array(fig, magnify/options.aa_factor, renderer)); - end - - % Set background to white (and set size) - set(fig, 'Color', 'w', 'Position', pos); - % Correct the colorbar axes colours - set(hCB(yCol==3), 'YColor', [1 1 1]); - set(hCB(xCol==3), 'XColor', [1 1 1]); - % Revert the black axes colors - set(hXs, 'XColor', [0,0,0]); - set(hYs, 'YColor', [0,0,0]); - set(hZs, 'ZColor', [0,0,0]); - set(hXrs, 'Color', [0,0,0]); - set(hYrs, 'Color', [0,0,0]); - set(hZrs, 'Color', [0,0,0]); + if options.transparent + if (options.png || options.alpha || options.gif || options.tif) + try %options.aa_factor < 4 % default, faster but lines are not anti-aliased + % If all pixels are indicated as opaque (i.e. something went wrong with the Java screen-capture) + isBgColor = A(:,:,1) == tcol(1) & ... + A(:,:,2) == tcol(2) & ... + A(:,:,3) == tcol(3); + % Set the bgcolor pixels to be fully-transparent + A(repmat(isBgColor,[1,1,3])) = 254; %=off-white % TODO: more memory efficient without repmat + alpha(isBgColor) = 0; + catch % older logic - much slower and causes figure flicker + if true % to fold the code below... + % Get out an alpha channel + % MATLAB "feature": black colorbar axes can change to white and vice versa! + hCB = findall(fig, 'Type','axes', 'Tag','Colorbar'); + if isempty(hCB) + yCol = []; + xCol = []; + else + yCol = get(hCB, 'YColor'); + xCol = get(hCB, 'XColor'); + if iscell(yCol) + yCol = cell2mat(yCol); + xCol = cell2mat(xCol); + end + yCol = sum(yCol, 2); + xCol = sum(xCol, 2); + end + % MATLAB "feature": apparently figure size can change when changing + % colour in -nodisplay mode + % Set the background colour to black, and set size in case it was + % changed internally + set(fig, 'Color', 'k', 'Position', pos); + % Correct the colorbar axes colours + set(hCB(yCol==0), 'YColor', [0 0 0]); + set(hCB(xCol==0), 'XColor', [0 0 0]); + % Correct black axes color to off-black (issue #249) + hAxes = findall(fig, 'Type','axes'); + [hXs,hXrs] = fixBlackAxle(hAxes, 'XColor'); + [hYs,hYrs] = fixBlackAxle(hAxes, 'YColor'); + [hZs,hZrs] = fixBlackAxle(hAxes, 'ZColor'); + + % The following code might cause out-of-memory errors + try + % Print large version to array + B = print2array(fig, magnify, renderer); + % Downscale the image + B = downsize(single(B), options.aa_factor); + catch + % This is more conservative in memory, but kills transparency (issue #58) + B = single(print2array(fig, magnify/options.aa_factor, renderer)); + end - % The following code might cause out-of-memory errors - try - % Print large version to array - A = print2array(fig, magnify, renderer); - % Downscale the image - A = downsize(single(A), options.aa_factor); - catch - % This is more conservative in memory, but kills transparency (issue #58) - A = single(print2array(fig, magnify/options.aa_factor, renderer)); - end + % Set background to white (and set size) + set(fig, 'Color', 'w', 'Position', pos); + % Correct the colorbar axes colours + set(hCB(yCol==3), 'YColor', [1 1 1]); + set(hCB(xCol==3), 'XColor', [1 1 1]); + % Revert the black axes colors + set(hXs, 'XColor', [0,0,0]); + set(hYs, 'YColor', [0,0,0]); + set(hZs, 'ZColor', [0,0,0]); + set(hXrs, 'Color', [0,0,0]); + set(hYrs, 'Color', [0,0,0]); + set(hZrs, 'Color', [0,0,0]); + + % The following code might cause out-of-memory errors + try + % Print large version to array + A = print2array(fig, magnify, renderer); + % Downscale the image + A = downsize(single(A), options.aa_factor); + catch + % This is more conservative in memory, but kills transparency (issue #58) + A = single(print2array(fig, magnify/options.aa_factor, renderer)); + end - % Workaround for issue #15 - szA = size(A); - szB = size(B); - if ~isequal(szA,szB) - A = A(1:min(szA(1),szB(1)), 1:min(szA(2),szB(2)), :); - B = B(1:min(szA(1),szB(1)), 1:min(szA(2),szB(2)), :); - if ~options.silent - warning('export_fig:bitmap:sizeMismatch','Problem detected by export_fig generation of a bitmap image; the generated export may look bad. Try to reduce the figure size to fit the screen, or avoid using export_fig''s -transparent option.') - end + % Workaround for issue #15 + szA = size(A); + szB = size(B); + if ~isequal(szA,szB) + A = A(1:min(szA(1),szB(1)), 1:min(szA(2),szB(2)), :); + B = B(1:min(szA(1),szB(1)), 1:min(szA(2),szB(2)), :); + if ~options.silent + warning('export_fig:bitmap:sizeMismatch','Problem detected by export_fig generation of a bitmap image; the generated export may look bad. Try to reduce the figure size to fit the screen, or avoid using export_fig''s -transparent option.') + end + end + % Compute the alpha map + alpha = round(sum(B - A, 3)) / (255 * 3) + 1; + A = alpha; + A(A==0) = 1; + A = B ./ A(:,:,[1 1 1]); + clear B + end %folded code... end - % Compute the alpha map - alpha = round(sum(B - A, 3)) / (255 * 3) + 1; - A = alpha; - A(A==0) = 1; - A = B ./ A(:,:,[1 1 1]); - clear B - end %folded code... + %A = uint8(A); + else % JPG,BMP + warning('export_fig:unsupported:background','Matlab cannot set transparency when exporting JPG/BMP image files (see imwrite function documentation)') end - %A = uint8(A); end % Downscale the image if its size was increased (for anti-aliasing) if size(A,1) > 1.1 * options.magnify * pixelpos(4) %1.1 to avoid edge-cases @@ -725,8 +774,8 @@ end if ~isempty(bitDepth) && bitDepth < 16 && size(A,3) == 3 % BitDepth specification requires using a color-map - [A, map] = rgb2ind(A, 256); - imwrite(A, map, pngOptions{:}); + [img, map] = rgb2ind(A, 256); + imwrite(img, map, pngOptions{:}); else imwrite(A, pngOptions{:}); end @@ -750,18 +799,84 @@ if options.tif % Save tif images in cmyk if wanted (and possible) if options.colourspace == 1 && size(A, 3) == 3 - A = double(255 - A); - K = min(A, [], 3); + img = double(255 - A); + K = min(img, [], 3); K_ = 255 ./ max(255 - K, 1); - C = (A(:,:,1) - K) .* K_; - M = (A(:,:,2) - K) .* K_; - Y = (A(:,:,3) - K) .* K_; - A = uint8(cat(3, C, M, Y, K)); + C = (img(:,:,1) - K) .* K_; + M = (img(:,:,2) - K) .* K_; + Y = (img(:,:,3) - K) .* K_; + img = uint8(cat(3, C, M, Y, K)); clear C M Y K K_ + else + img = A; + end + resolution = options.magnify * get(0,'ScreenPixelsPerInch'); + filename = [options.name '.tif']; + if options.transparent && any(alpha(:) < 1) && any(isBgColor(:)) + % Need to use low-level Tiff library since imwrite/writetif doesn't support alpha channel + alpha8 = uint8(alpha*255); + tag = ['Matlab ' version ' export_fig v' num2str(currentVersion)]; + mode = 'w'; if options.append, mode = 'a'; end + t = Tiff(filename,mode); %R2009a or newer + %See https://www.awaresystems.be/imaging/tiff/tifftags/baseline.html + t.setTag('ImageLength', size(img,1)); + t.setTag('ImageWidth', size(img,2)); + t.setTag('Photometric', Tiff.Photometric.RGB); + t.setTag('Compression', Tiff.Compression.Deflate); + t.setTag('PlanarConfiguration', Tiff.PlanarConfiguration.Chunky); + t.setTag('ExtraSamples', Tiff.ExtraSamples.AssociatedAlpha); + t.setTag('ResolutionUnit', Tiff.ResolutionUnit.Inch); + t.setTag('BitsPerSample', 8); + t.setTag('SamplesPerPixel',size(img,3)+1); %+1=alpha channel + t.setTag('XResolution', resolution); + t.setTag('YResolution', resolution); + t.setTag('Software', tag); + t.write(cat(3,img,alpha8)); + t.close; + else + % Use the builtin imwrite/writetif function + append_mode = {'overwrite', 'append'}; + mode = append_mode{options.append+1}; + format_options = getFormatOptions(options, 'tif'); %Issue #269 + imwrite(img, filename, 'Resolution',resolution, 'WriteMode',mode, format_options{:}); end + end + if options.gif + % TODO - merge contents with im2gif.m + % Convert to color-map image required by GIF specification + [img, map] = rgb2ind(A, 256); + % Handle the case of trying to append to non-existing GIF file + % (imwrite() croaks when asked to append to a non-existing file) + filename = [options.name '.gif']; + options.append = options.append && existFile(filename); + % Set the default GIF options for imwrite() append_mode = {'overwrite', 'append'}; - format_options = getFormatOptions(options, 'tif'); %Issue #269 - imwrite(A, [options.name '.tif'], 'Resolution',options.magnify*get(0,'ScreenPixelsPerInch'), 'WriteMode',append_mode{options.append+1}, format_options{:}); + writeMode = append_mode{options.append+1}; + gifOptions = {'WriteMode',writeMode}; + if options.transparent % only use alpha channel if -transparent was requested + exp = 256 .^ (0:2); + mapVals = sum(round(map*255).*exp,2); + tcolVal = sum(round(double(tcol)).*exp); + alphaIdx = find(mapVals==tcolVal,1); + if isempty(alphaIdx) || alphaIdx <= 0, alphaIdx = 1; end + % GIF color index of uint8/logical images starts at 0, not 1 + if ~isfloat(img), alphaIdx = alphaIdx - 1; end + gifOptions = [gifOptions, 'TransparentColor',alphaIdx, ... + 'DisposalMethod','restoreBG']; + else + alphaIdx = 1; + end + if ~options.append + % LoopCount and BackgroundColor can only be specified in the + % 1st GIF frame (not in append mode) + % Set default LoopCount=65535 to enable looping within MS Office + gifOptions = [gifOptions, 'LoopCount',65535, 'BackgroundColor',alphaIdx]; + end + % Set GIF-specific options specified by the user (if any) + format_options = getFormatOptions(options, 'gif'); + gifOptions = [gifOptions, format_options{:}]; + % Save the gif file + imwrite(img, map, filename, gifOptions{:}); end end @@ -809,17 +924,12 @@ fwrite(fid,1); fclose(fid); delete(tmp_nam); - isTempDirOk = true; + pdf_nam_tmp = [tempname '.pdf']; catch % Temp dir is not writable, so use the user-specified folder [dummy,fname,fext] = fileparts(tmp_nam); %#ok fpath = fileparts(options.name); tmp_nam = fullfile(fpath,[fname fext]); - isTempDirOk = false; - end - if isTempDirOk - pdf_nam_tmp = [tempname '.pdf']; - else pdf_nam_tmp = fullfile(fpath,[fname '.pdf']); end if options.pdf @@ -839,7 +949,7 @@ end if ~options.crop % Issue #56: due to internal bugs in Matlab's print() function, we can't use its internal cropping mechanism, - % therefore we always use '-loose' (in print2eps.m) and do our own cropping (in crop_borders) + % therefore we always use '-loose' (in print2eps.m) and do our own cropping (with crop_borders.m) %printArgs{end+1} = '-loose'; end if any(strcmpi(varargin,'-depsc')) @@ -883,7 +993,7 @@ end end % Generate an eps - print2eps(tmp_nam, fig, options, printArgs{:}); + print2eps(tmp_nam, fig, options, printArgs{:}); %winopen(tmp_nam) % { % Remove the background, if desired if options.transparent %&& ~isequal(get(fig, 'Color'), 'none') @@ -925,8 +1035,13 @@ end catch % Alert in case of error creating output PDF/EPS file (issue #179) - if exist(pdf_nam_tmp, 'file') - errMsg = ['Could not create ' pdf_nam ' - perhaps the folder does not exist, or you do not have write permissions, or the file is open in another application']; + if existFile(pdf_nam_tmp) + fpath = fileparts(pdf_nam); + if ~isempty(fpath) && exist(fpath,'dir')==0 + errMsg = ['Could not create ' pdf_nam ' - folder "' fpath '" does not exist']; + else % output folder exists + errMsg = ['Could not create ' pdf_nam ' - perhaps you do not have write permissions, or the file is open in another application']; + end error('export_fig:PDF:create',errMsg); else error('export_fig:NoEPS','Could not generate the intermediary EPS file.'); @@ -990,29 +1105,36 @@ if options.svg filename = [options.name '.svg']; % Adapted from Dan Joshea's https://github.com/djoshea/matlab-save-figure : - try %if verLessThan('matlab', '8.4') - % Try using the fig2svg/plot2svg utilities + try %if ~verLessThan('matlab', '8.4') + % Try Matlab's built-in svg engine (from Batik Graphics2D for java) + set(fig,'Units','pixels'); % All data in the svg-file is saved in pixels + printArgs = {renderer}; + if ~isempty(options.resolution) + printArgs{end+1} = sprintf('-r%d', options.resolution); + end try - fig2svg(filename, fig); %https://github.com/kupiqu/fig2svg + print(fig, '-dsvg', printArgs{:}, filename); catch - plot2svg(filename, fig); %https://github.com/jschwizer99/plot2svg - if ~options.silent - warning('export_fig:SVG:plot2svg', 'export_fig used the plot2svg utility for SVG output. Better results may be gotten via the fig2svg utility (https://github.com/kupiqu/fig2svg).'); - end + % built-in print() failed, try saveas() + % Note: saveas() currently just calls print(fig,filename,'-dsvg') + % so since print() failed, saveas() will probably also fail + saveas(fig, filename); end - catch %else % (neither fig2svg nor plot2svg are available) - % Try Matlab's built-in svg engine (from Batik Graphics2D for java) + if ~options.silent + warning('export_fig:SVG:print', 'export_fig used Matlab''s built-in SVG output engine. Better results may be gotten via the fig2svg utility (https://github.com/kupiqu/fig2svg).'); + end + catch %else % built-in print()/saveas() failed - maybe an old Matlab release (no -dsvg) + % Try using the fig2svg/plot2svg utilities try - set(fig,'Units','pixels'); % All data in the svg-file is saved in pixels - printArgs = {renderer}; - if ~isempty(options.resolution) - printArgs{end+1} = sprintf('-r%d', options.resolution); - end - print(fig, '-dsvg', printArgs{:}, filename); - if ~options.silent - warning('export_fig:SVG:print', 'export_fig used Matlab''s built-in SVG output engine. Better results may be gotten via the fig2svg utility (https://github.com/kupiqu/fig2svg).'); + try + fig2svg(filename, fig); %https://github.com/kupiqu/fig2svg + catch + plot2svg(filename, fig); %https://github.com/jschwizer99/plot2svg + if ~options.silent + warning('export_fig:SVG:plot2svg', 'export_fig used the plot2svg utility for SVG output. Better results may be gotten via the fig2svg utility (https://github.com/kupiqu/fig2svg).'); + end end - catch err % built-in print() failed - maybe an old Matlab release (no -dsvg) + catch err filename = strrep(filename,'export_fig_out','filename'); msg = ['SVG output is not supported for your figure: ' err.message '\n' ... 'Try one of the following alternatives:\n' ... @@ -1099,7 +1221,14 @@ try set(Hlims(a), 'XLimMode', Xlims{a}, 'YLimMode', Ylims{a}, 'ZLimMode', Zlims{a},... 'XTickMode', Xtick{a}, 'YTickMode', Ytick{a}, 'ZTickMode', Ztick{a},... - 'XTickLabelMode', Xlabel{a}, 'YTickLabelMode', Ylabel{a}, 'ZTickLabelMode', Zlabel{a}); + 'XTickLabelMode', Xlabel{a}, 'YTickLabelMode', Ylabel{a}, 'ZTickLabelMode', Zlabel{a}); + try % only in R2021a+ + set(Hlims(a), 'XTickLabelRotationMode', Xtkrot{a}, ... + 'YTickLabelRotationMode', Ytkrot{a}, ... + 'ZTickLabelRotationMode', Ztkrot{a}); + catch + % ignore - possibly R2020b or earlier + end catch % ignore - fix issue #4 (using HG2 on R2014a and earlier) end @@ -1121,22 +1250,6 @@ % Output to clipboard (if requested) if options.clipboard - % Delete the output file if unchanged from the default name ('export_fig_out.png') - if strcmpi(options.name,'export_fig_out') - try - fileInfo = dir('export_fig_out.png'); - if ~isempty(fileInfo) - timediff = now - fileInfo.datenum; - ONE_SEC = 1/24/60/60; - if timediff < ONE_SEC - delete('export_fig_out.png'); - end - end - catch - % never mind... - end - end - % Use Java clipboard by default if strcmpi(options.clipformat,'image') % Save the image in the system clipboard @@ -1147,7 +1260,7 @@ if ~options.silent warning('export_fig:clipboardJava', 'export_fig -clipboard output failed: requires Java to work'); end - return; + return end try % Import necessary Java classes @@ -1226,6 +1339,25 @@ end end + % Delete the output file if unchanged from the default name ('export_fig_out.png') + % and clipboard, toolbar, and/or menubar were requested + if options.clipboard || options.toolbar || options.menubar + if strcmpi(options.name,'export_fig_out') + try + fileInfo = dir('export_fig_out.png'); + if ~isempty(fileInfo) + timediff = now - fileInfo.datenum; + ONE_SEC = 1/24/60/60; + if timediff < ONE_SEC + delete('export_fig_out.png'); + end + end + catch + % never mind... + end + end + end + % Don't output the data to console unless requested if ~nargout clear imageData alpha @@ -1249,7 +1381,9 @@ end url = 'https://github.com/altmany/export_fig/issues/285#issuecomment-815008561'; urlStr = hyperlink(url,'details'); - fprintf(2,'Export_fig transparancy is not supported by your Ghostscript version%s. \nInstall GS version 9.28 or earlier to use transparency (%s).\n', verStr, urlStr); + errMsg = sprintf('Transparancy is not supported by your export_fig (%s) and Ghostscript%s versions. \nInstall GS version 9.28 or earlier to use transparency (%s).', num2str(currentVersion), verStr, urlStr); + %fprintf(2,'%s\n',errMsg); + error('export_fig:setopacityalpha',errMsg) %#ok elseif displaySuggestedWorkarounds && ~strcmpi(err.message,'export_fig error') isNewerVersionAvailable = checkForNewerVersion(currentVersion); % alert if a newer version exists if isempty(regexpi(err.message,'Ghostscript')) %#ok @@ -1297,7 +1431,7 @@ % ignore - maybe an old MAtlab release end fprintf(2, '\nIf the problem persists, then please %s.\n', hyperlink('https://github.com/altmany/export_fig/issues','report a new issue')); - if exist(tmp_nam,'file') + if existFile(tmp_nam) fprintf(2, 'In your report, please upload the problematic EPS file: %s (you can then delete this file).\n', tmp_nam); end fprintf(2, '\n'); @@ -1322,6 +1456,7 @@ 'tif', false, ... 'jpg', false, ... 'bmp', false, ... + 'gif', false, ... 'clipboard', false, ... 'clipformat', 'image', ... 'colourspace', 0, ... % 0: RGB/gray, 1: CMYK, 2: gray @@ -1345,6 +1480,8 @@ 'preserve_size', false, ... 'silent', false, ... 'regexprep', [], ... + 'toolbar', false, ... + 'menubar', false, ... 'gs_options', {{}}); end @@ -1356,7 +1493,8 @@ % Set the defaults native = false; % Set resolution to native of an image - options = default_options(); + defaultOptions = default_options(); + options = defaultOptions; options.im = (nout == 1); % user requested imageData output options.alpha = (nout == 2); % user requested alpha output options.handleName = ''; % default handle name @@ -1368,12 +1506,13 @@ skipNext = skipNext-1; continue; end - if all(ishandle(varargin{a})) - fig = varargin{a}; + thisArg = varargin{a}; + if all(ishandle(thisArg)) + fig = thisArg; options.handleName = argNames{a}; - elseif ischar(varargin{a}) && ~isempty(varargin{a}) - if varargin{a}(1) == '-' - switch lower(varargin{a}(2:end)) + elseif ischar(thisArg) && ~isempty(thisArg) + if thisArg(1) == '-' + switch lower(thisArg(2:end)) case 'nocrop' options.crop = false; options.crop_amounts = [0,0,0,0]; @@ -1408,7 +1547,7 @@ case {'gray', 'grey'} options.colourspace = 2; case {'a1', 'a2', 'a3', 'a4'} - options.aa_factor = str2double(varargin{a}(3)); + options.aa_factor = str2double(thisArg(3)); case 'append' options.append = true; case 'bookmark' @@ -1451,12 +1590,20 @@ inputOptions = varargin{a+1}; %options.format_options = inputOptions; if isempty(inputOptions), continue, end - formats = fieldnames(inputOptions(1)); - for idx = 1 : numel(formats) - optionsStruct = inputOptions.(formats{idx}); - %optionsCells = [fieldnames(optionsStruct) struct2cell(optionsStruct)]'; - formatName = regexprep(lower(formats{idx}),{'tiff','jpeg'},{'tif','jpg'}); - options.format_options.(formatName) = optionsStruct; %=optionsCells(:)'; + if iscell(inputOptions) + fields = inputOptions(1:2:end)'; + values = inputOptions(2:2:end)'; + options.format_options.all = cell2struct(values, fields); + elseif isstruct(inputOptions) + formats = fieldnames(inputOptions(1)); + for idx = 1 : numel(formats) + optionsStruct = inputOptions.(formats{idx}); + %optionsCells = [fieldnames(optionsStruct) struct2cell(optionsStruct)]'; + formatName = regexprep(lower(formats{idx}),{'tiff','jpeg'},{'tif','jpg'}); + options.format_options.(formatName) = optionsStruct; %=optionsCells(:)'; + end + else + warning('export_fig:options','export_fig -options argument is not in the expected format - ignored'); end skipNext = 1; case 'silent' @@ -1464,22 +1611,26 @@ case 'regexprep' options.regexprep = varargin(a+1:a+2); skipNext = 2; + case 'toolbar' + options.toolbar = true; + case 'menubar' + options.menubar = true; otherwise try wasError = false; - if strcmpi(varargin{a}(1:2),'-d') - varargin{a}(2) = 'd'; % ensure lowercase 'd' - options.gs_options{end+1} = varargin{a}; - elseif strcmpi(varargin{a}(1:2),'-c') - if strncmpi(varargin{a},'-clipboard:',11) + if strcmpi(thisArg(1:2),'-d') + thisArg(2) = 'd'; % ensure lowercase 'd' + options.gs_options{end+1} = thisArg; + elseif strcmpi(thisArg(1:2),'-c') + if strncmpi(thisArg,'-clipboard:',11) wasError = true; - error('export_fig:BadOptionValue','option ''%s'' cannot be parsed: only image, bitmap, emf and pdf formats are supported',varargin{a}); + error('export_fig:BadOptionValue','option ''%s'' cannot be parsed: only image, bitmap, emf and pdf formats are supported',thisArg); end - if numel(varargin{a})==2 + if numel(thisArg)==2 skipNext = 1; vals = str2num(varargin{a+1}); %#ok else - vals = str2num(varargin{a}(3:end)); %#ok + vals = str2num(thisArg(3:end)); %#ok end if numel(vals)~=4 wasError = true; @@ -1488,7 +1639,7 @@ options.crop_amounts = vals; options.crop = true; else % scalar parameter value - val = str2double(regexp(varargin{a}, '(?<=-(m|M|r|R|q|Q|p|P))-?\d*.?\d+', 'match')); + val = str2double(regexp(thisArg, '(?<=-(m|M|r|R|q|Q|p|P))-?\d*.?\d+', 'match')); if isempty(val) || isnan(val) % Issue #51: improved processing of input args (accept space between param name & value) val = str2double(varargin{a+1}); @@ -1498,9 +1649,9 @@ end if ~isscalar(val) || isnan(val) wasError = true; - error('export_fig:BadOptionValue','option %s is not recognised or cannot be parsed', varargin{a}); + error('export_fig:BadOptionValue','option %s is not recognised or cannot be parsed', thisArg); end - switch lower(varargin{a}(2)) + switch lower(thisArg(2)) case 'm' % Magnification may never be negative if val <= 0 @@ -1521,16 +1672,50 @@ if wasError % intentional raise rethrow(err) else % unintentional - error('export_fig:BadOption',['Unrecognized export_fig input option: ''' varargin{a} '''']); + error('export_fig:BadOption',['Unrecognized export_fig input option: ''' thisArg '''']); end end end else - [p, options.name, ext] = fileparts(varargin{a}); - if ~isempty(p) + % test for case of figure name rather than export filename + isFigName = false; + if isempty(options.handleName) + try + if strncmpi(thisArg,'Figure',6) + [~,~,~,~,e] = regexp(thisArg,'figure\s*(\d+)\s*(:\s*(.*))?','ignorecase'); + figNumber = str2double(e{1}{1}); + figName = regexprep(e{1}{2},':\s*',''); + findProps = {'Number',figNumber}; + if ~isempty(figName) + findProps = [findProps,'Name',figName]; %#ok + end + else + findProps = {'Name',thisArg}; + end + possibleFig = findall(0,'-depth',1,'Type','figure',findProps{:}); + if ~isempty(possibleFig) + fig = possibleFig(1); % return the 1st figure found + if ~strcmpi(options.name, defaultOptions.name) + continue % export fname was already specified + else + isFigName = true; %use figure name as export fname + end + end + catch + % ignore - treat as export filename, not figure name + end + end + % parse the input as a filename, alert if requested folder does not exist + [p, options.name, ext] = fileparts(thisArg); + if ~isempty(p) % export folder name/path was specified % Issue #221: alert if the requested folder does not exist - if ~exist(p,'dir'), error('export_fig:BadPath',['Folder ' p ' does not exist!']); end - options.name = [p filesep options.name]; + if exist(p,'dir') + options.name = fullfile(p, options.name); + elseif ~isFigName + error('export_fig:BadPath','Folder %s does not exist, nor is it the name of any active figure!',p); + else % isFigName + % specified a figure name so ignore the bad folder part + end end switch lower(ext) case {'.tif', '.tiff'} @@ -1549,10 +1734,10 @@ options.pdf = true; case '.fig' % If no open figure, then load the specified .fig file and continue - figFilename = varargin{a}; + figFilename = thisArg; if isempty(fig) fig = openfig(figFilename,'invisible'); - varargin{a} = fig; + %varargin{a} = fig; options.closeFig = true; options.handleName = ['openfig(''' figFilename ''')']; else @@ -1563,8 +1748,10 @@ end case '.svg' options.svg = true; + case '.gif' + options.gif = true; otherwise - options.name = varargin{a}; + options.name = thisArg; end end end @@ -1749,19 +1936,23 @@ function eps_remove_background(fname, count) fclose(fh); end -function b = isvector(options) +function b = isvector(options) % this only includes EPS-based vector formats (so not SVG,EMF) b = options.pdf || options.eps; end function b = isbitmap(options) - b = options.png || options.tif || options.jpg || options.bmp || options.im || options.alpha; + b = options.png || options.tif || options.jpg || options.bmp || ... + options.gif || options.im || options.alpha; end function [A, tcol, alpha] = getFigImage(fig, magnify, renderer, options, pos) if options.transparent % MATLAB "feature": figure size can change when changing color in -nodisplay mode - set(fig, 'Color', 'w', 'Position', pos); - drawnow; % repaint figure, otherwise Java screencapture will see black bgcolor + % Note: figure background is set to off-white, not 'w', to handle common white elements (issue #330) + set(fig, 'Color',254/255*[1,1,1], 'Position',pos); + % repaint figure, otherwise Java screencapture will see black bgcolor + % Yair 19/12/21 - unnecessary: drawnow is called at top of print2array + %drawnow; end % Print large version to array try @@ -1769,12 +1960,12 @@ function eps_remove_background(fname, count) [A, tcol, alpha] = print2array(fig, magnify, renderer); catch % This is more conservative in memory, but perhaps kills transparency(?) - [A, tcol, alpha] = print2array(fig, magnify/options.aa_factor, renderer); + [A, tcol, alpha] = print2array(fig, magnify/options.aa_factor, renderer, 'retry'); end % In transparent mode, set the bgcolor to white if options.transparent % Note: tcol should already be [255,255,255] here, but just in case it's not... - tcol = uint8([255,255,255]); %=white + tcol = uint8(254*[1,1,1]); %=off-white end end @@ -1818,7 +2009,10 @@ function add_bookmark(fname, bookmark_text) fclose(fh); end -function set_tick_mode(Hlims, ax) +function set_manual_axes_modes(Hlims, ax) + % Set the axes limits mode to manual + set(Hlims, [ax 'LimMode'], 'manual'); + % Set the tick mode of linear axes to manual % Leave log axes alone as these are tricky M = get(Hlims, [ax 'Scale']); @@ -1836,6 +2030,14 @@ function set_tick_mode(Hlims, ax) props = {[ax 'TickMode'],'manual', [ax 'TickLabelMode'],'manual'}; tickVals = get(hAxes,[ax 'Tick']); tickStrs = get(hAxes,[ax 'TickLabel']); + try % TickLabelRotation is available since R2021a + propName = [ax,'TickLabelRotationMode']; + if ~isempty(get(hAxes,propName)) %this will croak in R2020b- + props = [props, propName,'manual']; %#ok + end + catch + % ignore - probably R2020b or older + end try % Fix issue #236 exponents = [hAxes.([ax 'Axis']).SecondaryLabel]; catch @@ -1903,9 +2105,14 @@ function change_rgb_to_cmyk(fname) % convert RGB => CMYK within an EPS file try optionsStruct = options.format_options.(lower(formatName)); catch - % User did not specify any extra parameters for this format - optionsCells = {}; - return + try + % Perhaps user specified the options in cell array format + optionsStruct = options.format_options.all; + catch + % User did not specify any extra parameters for this format + optionsCells = {}; + return + end end optionNames = fieldnames(optionsStruct); optionVals = struct2cell(optionsStruct); @@ -2245,6 +2452,9 @@ function displayMsg(params, funcName, type, filenameParam) handleName = 'hFigure'; end msg = ['In Matlab R2020a+ you can also use ' funcName '(' handleName filenameParam params ') for simple ' type ' export']; + if ~isempty(strfind(params,'''vector''')) %#ok + msg = [msg ', which could also improve image vectorization, solving rasterization/pixelization problems.']; + end oldWarn = warning('on','verbose'); warning(['export_fig:' funcName], msg); warning(oldWarn); @@ -2253,3 +2463,203 @@ function displayMsg(params, funcName, type, filenameParam) end end end + +% Does a file exist? +function flag = existFile(filename) + try + % isfile() is faster than exist(), but does not report files on path + flag = isfile(filename); + catch + flag = exist(filename,'file') ~= 0; + end +end + +% Add interactive export button to the figure's toolbar +function addToolbarButton(hFig, options) + % Ensure we have a valid toolbar handle + if isempty(hFig) + if options.silent + return + else + error('export_fig:noFigure','not a valid GUI handle'); + end + end + set(hFig,'ToolBar','figure'); + hToolbar = findall(hFig, 'type','uitoolbar', '-depth',1); + if isempty(hToolbar) + if ~options.silent + warning('export_fig:noToolbar','cannot add toolbar button to the specified figure'); + end + end + hToolbar = hToolbar(1); % just in case there are several toolbars... - use only the first + + % Bail out silently if the export_fig button already exists + hButton = findall(hToolbar, 'Tag','export_fig'); + if ~isempty(hButton) + return + end + + % Prepare the camera icon + icon = ['3333333333333333'; ... + '3333333333333333'; ... + '3333300000333333'; ... + '3333065556033333'; ... + '3000000000000033'; ... + '3022222222222033'; ... + '3022220002222033'; ... + '3022203110222033'; ... + '3022201110222033'; ... + '3022204440222033'; ... + '3022220002222033'; ... + '3022222222222033'; ... + '3000000000000033'; ... + '3333333333333333'; ... + '3333333333333333'; ... + '3333333333333333']; + cm = [ 0 0 0; ... % black + 0 0.60 1; ... % light blue + 0.53 0.53 0.53; ... % light gray + NaN NaN NaN; ... % transparent + 0 0.73 0; ... % light green + 0.27 0.27 0.27; ... % gray + 0.13 0.13 0.13]; % dark gray + cdata = ind2rgb(uint8(icon-'0'),cm); + + % If the button does not already exit + tooltip = 'Export this figure'; + + % Add the button with the icon to the figure's toolbar + props = {'Parent',hToolbar, 'CData',cdata, 'Tag','export_fig', ... + 'Tooltip',tooltip, 'ClickedCallback',@interactiveExport}; + try + hButton = []; % just in case we croak below + + % Create a new split-button with the open-file button's data + oldWarn = warning('off','MATLAB:uisplittool:DeprecatedFunction'); + hButton = uisplittool(props{:}); + warning(oldWarn); + + % Add the split-button's menu items + drawnow; pause(0.01); % allow the buttom time to render + jButton = get(hButton,'JavaContainer'); %#ok + jButtonMenu = jButton.getMenuComponent; + + tooltip = [tooltip ' (specify filename/format)']; + try jButtonMenu.setToolTipText(tooltip); catch, end + try jButton.getComponentPeer.getComponent(1).setToolTipText(tooltip); catch, end + + defaultFname = get(hFig,'Name'); + if isempty(defaultFname), defaultFname = 'figure'; end + imFormats = {'pdf','eps','emf','svg','png','tif','jpg','bmp','gif'}; + for idx = 1 : numel(imFormats) + thisFormat = imFormats{idx}; + filename = [defaultFname '.' thisFormat]; + label = [upper(thisFormat) ' image file (' filename ')']; + jMenuItem = handle(jButtonMenu.add(label),'CallbackProperties'); + set(jMenuItem,'ActionPerformedCallback',@(h,e)export_fig(hFig,filename)); + end + jButtonMenu.addSeparator(); + cbFormats = {'image','bitmap','meta','pdf'}; + for idx = 1 : numel(cbFormats) + thisFormat = cbFormats{idx}; + exFormat = ['-clipboard:' thisFormat]; + label = ['Clipboard (' thisFormat ' format)']; + jMenuItem = handle(jButtonMenu.add(label),'CallbackProperties'); + set(jMenuItem,'ActionPerformedCallback',@(h,e)export_fig(hFig,exFormat)); + end + jButtonMenu.addSeparator(); + jMenuItem = handle(jButtonMenu.add('Select filename and format'),'CallbackProperties'); + set(jMenuItem,'ActionPerformedCallback',@(h,e)interactiveExport(hFig)); + catch % revert to a simple documented toolbar pushbutton + warning(oldWarn); + if isempty(hButton) %avoid duplicate toolbar buttons (keep the splittool) + hButton = uipushtool(props{:}); %#ok + end + end +end + +% Add interactive export menu to the figure's menubar +function addMenubarMenu(hFig, options) + % Ensure we have a valid figure handle + if isempty(hFig) + if options.silent + return + else + error('export_fig:noFigure','not a valid GUI handle'); + end + end + set(hFig,'MenuBar','figure'); + + % Bail out silently if the export_fig menu already exists + hMainMenu = findall(hFig, '-depth',1, 'type','uimenu', 'Tag','export_fig'); + if ~isempty(hMainMenu) + return + end + + % Add the export_fig menu to the figure's menubar + hMainMenu = uimenu(hFig, 'Text','E&xport', 'Tag','export_fig'); + defaultFname = get(hFig,'Name'); + if isempty(defaultFname), defaultFname = 'figure'; end + imFormats = {'pdf','eps','emf','svg','png','tif','jpg','bmp','gif'}; + for idx = 1 : numel(imFormats) + thisFormat = imFormats{idx}; + filename = [defaultFname '.' thisFormat]; + label = [upper(thisFormat) ' image file (' filename ')']; + uimenu(hMainMenu, 'Text',label, 'MenuSelectedFcn',@(h,e)export_fig(hFig,filename)); + end + cbFormats = {'image','bitmap','meta','pdf'}; + for idx = 1 : numel(cbFormats) + thisFormat = cbFormats{idx}; + exFormat = ['-clipboard:' thisFormat]; + label = ['Clipboard (' thisFormat ' format)']; + sep = 'off'; if idx==1, sep = 'on'; end + uimenu(hMainMenu, 'Text',label, 'Separator',sep, ... + 'MenuSelectedFcn',@(h,e)export_fig(hFig,exFormat)); + end + uimenu(hMainMenu, 'Text','Select filename and format', 'Separator','on', ... + 'MenuSelectedFcn',@interactiveExport); +end + +% Callback functions for toolbar/menubar actions +function interactiveExport(hObject, varargin) + % Get the exported figure handle + hFig = gcbf; + if isempty(hFig) + hFig = ancestor(hObject, 'figure'); + end + if isempty(hFig) + return % bail out silently if no figure is available + end + + % Display a Save-as dialog to let the user select the export name & type + defaultFname = get(hFig,'Name'); + if isempty(defaultFname), defaultFname = 'figure'; end + %formats = imformats; + formats = {'pdf','eps','emf','svg','png','tif','jpg','bmp','gif', ... + 'clipboard:image','clipboard:bitmap','clipboard:meta','clipboard:pdf'}; + for idx = 1 : numel(formats) + thisFormat = formats{idx}; + ext = sprintf('*.%s',thisFormat); + if ~any(thisFormat==':') % image file format + description = [upper(thisFormat) ' image file (' ext ')']; + format(idx,1:2) = {ext, description}; %#ok + else % clipboard format + description = [strrep(thisFormat,':',' (') ' format *.)']; + format(idx,1:2) = {'*.*', description}; %#ok + end + end + %format + [filename,pathname,idx] = uiputfile(format,'Save figure export as',defaultFname); + drawnow; pause(0.01); % prevent a Matlab hang + if ~isequal(filename,0) + thisFormat = formats{idx}; + if ~any(thisFormat==':') % export to image file + filename = fullfile(pathname,filename); + export_fig(hFig, filename); + else % export to clipboard + export_fig(hFig, ['-' thisFormat]); + end + else + % User canceled the dialog - bail out silently + end +end diff --git a/export_fig/isolate_axes.m b/export_fig/isolate_axes.m index 55c5e8c..fe3ada3 100644 --- a/export_fig/isolate_axes.m +++ b/export_fig/isolate_axes.m @@ -31,6 +31,7 @@ % on FEX page as a comment on 24-Apr-2014); standardized indentation & help section % 22/04/15: Bug fix: legends and colorbars were not exported when exporting axes handle in HG2 % 02/02/21: Fix axes, figure size to preserve input axes image resolution (thanks @Optecks) +% 25/10/21: Bug fix: subplots were not isolated properly leading to print error (issue #347) %} % Make sure we have an array of handles @@ -64,9 +65,12 @@ % of the Input Axes (thanks @Optecks) allaxes = findall(fh, 'type', 'axes'); if ~isempty(ah) - sz = get(ah(1), 'Position'); - set(allaxes(1), 'Position', [0 0 sz(3) sz(4)]); - set(fh, 'Position', [0 0 sz(3) sz(4)]); + sz = get(ah(1), 'OuterPosition'); + un = get(ah(1), 'Units'); + set(allaxes(1), 'Units',un, 'OuterPosition', [0 0 sz(3) sz(4)]); + set(allaxes(1), 'Units','pixels'); + sz = get(allaxes(1), 'OuterPosition'); + set(fh, 'Units','pixels', 'Position',[0 0 sz(3) sz(4)]+1); end if nargin < 2 || ~vis @@ -111,7 +115,7 @@ catch leg_pos = get(lh, 'Position'); % No OuterPosition in HG2, only in HG1 end - if nLeg > 1; + if nLeg > 1 leg_pos = cell2mat(leg_pos); end leg_pos(:,3:4) = leg_pos(:,3:4) + leg_pos(:,1:2); @@ -144,7 +148,7 @@ for a = 1:numel(ah) h = get(ah(a), 'parent'); while h ~= 0 - ph = [ph; h]; + ph = [ph; h]; %#ok h = get(h, 'parent'); end end diff --git a/export_fig/print2array.m b/export_fig/print2array.m index 126b7f7..91cf719 100644 --- a/export_fig/print2array.m +++ b/export_fig/print2array.m @@ -61,6 +61,10 @@ % 07/10/20: Use JavaFrame_I where possible, to avoid evoking a JavaFrame warning % 07/03/21: Fixed edge-case in case a non-figure handle was provided as input arg % 10/03/21: Forced a repaint at top of function to ensure accurate image snapshot (issue #211) +% 26/08/21: Added a short pause to avoid unintended image cropping (issue #318) +% 25/10/21: Avoid duplicate error message when retrying print2array with different resolution; display internal print error message +% 19/12/21: Speedups; fixed exporting non-current figure (hopefully fixes issue #318) +% 22/12/21: Avoid memory leak during screen-capture %} % Generate default input arguments, if needed @@ -76,6 +80,8 @@ px = get(fig, 'Position'); set(fig, 'Units', old_mode); + pause(0.02); % add a short pause to avoid unintended cropping (issue #318) + % Retrieve the background colour bcol = get(fig, 'Color'); try @@ -112,8 +118,12 @@ isTempDirOk = false; end % Enable users to specify optional ghostscript options (issue #36) + isRetry = false; if nargin > 3 && ~isempty(gs_options) - if iscell(gs_options) + if isequal(gs_options,'retry') + isRetry = true; + gs_options = ''; + elseif iscell(gs_options) gs_options = sprintf(' %s',gs_options{:}); elseif ~ischar(gs_options) error('gs_options input argument must be a string or cell-array of strings'); @@ -174,7 +184,9 @@ % Throw any error that occurred if err % Display suggested workarounds to internal print() error (issue #16) - fprintf(2, 'An error occured with Matlab''s builtin print function.\nTry setting the figure Renderer to ''painters'' or use opengl(''software'').\n\n'); + if ~isRetry + fprintf(2, 'An error occurred in Matlab''s builtin print function:\n%s\nTry setting the figure Renderer to ''painters'' or use opengl(''software'').\n\n', ex.message); + end rethrow(ex); end end @@ -279,7 +291,8 @@ import java.awt.image.BufferedImage try TYPE_INT_RGB = BufferedImage.TYPE_INT_RGB; catch, TYPE_INT_RGB = 1; end jImage = BufferedImage(w, h, TYPE_INT_RGB); - jPanel.paint(jImage.createGraphics); + jGraphics = jImage.createGraphics; + jPanel.paint(jGraphics); jPanel.paint(jOriginalGraphics); % repaint original figure to avoid a blank window % Extract the RGB pixels from the BufferedImage (see screencapture.m) @@ -292,8 +305,11 @@ % And now also the alpha channel (if available) alpha = transpose(reshape(pixelsData(4, :, :), w, h)); + % Avoid memory leaks (see \toolbox\matlab\toolstrip\+matlab\+ui\+internal\+toolstrip\Icon.m>localFromImgToURL) + jGraphics.dispose(); + % Ensure that the results are the expected size, otherwise raise an error - figSize = getpixelposition(gcf); + figSize = getpixelposition(hFig); expectedSize = [figSize(4), figSize(3), 3]; if ~isequal(expectedSize, size(imgData)) error('bad Java screen-capture size!') diff --git a/export_fig/print2eps.m b/export_fig/print2eps.m index 5e70300..8f42fef 100644 --- a/export_fig/print2eps.m +++ b/export_fig/print2eps.m @@ -111,6 +111,9 @@ function print2eps(name, fig, export_options, varargin) % 10/12/20: Enabled user-specified regexp replacements in the generated EPS file (issue #324) % 11/03/21: Added documentation about export_options.regexprep; added sanity check (issue #324) % 21/07/21: Fixed misleading warning message about regexprep field when it's empty (issue #338) +% 26/08/21: Added a short pause to avoid unintended image cropping (issue #318) +% 16/03/22: Fixed occasional empty files due to excessive cropping (issues #350, #351) +% 15/05/22: Fixed EPS bounding box (issue #356) %} options = {'-loose'}; @@ -323,6 +326,9 @@ function print2eps(name, fig, export_options, varargin) origAlphaColors = eps_maintainAlpha(fig); end + % Ensure that everything is fully rendered, to avoid cropping (issue #318) + drawnow; pause(0.02); + % Print to eps file print(fig, options{:}, name); @@ -536,8 +542,16 @@ function print2eps(name, fig, export_options, varargin) % 2. Create a bitmap image and use crop_borders to create the relative % bb with respect to the PageBoundingBox + drawnow; pause(0.05); % avoid unintended cropping (issue #318) [A, bcol] = print2array(fig, 1, renderer); [aa, aa, aa, bb_rel] = crop_borders(A, bcol, bb_padding, crop_amounts); %#ok + if any(bb_rel>1) || any(bb_rel<=0) % invalid cropping - retry after prolonged pause + pause(0.15); % avoid unintended cropping (issues #350, #351) + [A, bcol] = print2array(fig, 1, renderer); + [aa, aa, aa, bb_rel] = crop_borders(A, bcol, bb_padding, crop_amounts); %#ok + end + bb_rel(bb_rel>1) = 1; % ignore invalid values + bb_rel(bb_rel<0) = 1; % ignore invalid values (fix issue #356) try set(hTitles,'Color','k'); catch, end @@ -586,6 +600,8 @@ function print2eps(name, fig, export_options, varargin) % Write out the fixed eps file read_write_entire_textfile(name, fstrm); + + drawnow; pause(0.01); end function [StoredColors, fstrm, foundFlags] = eps_maintainAlpha(fig, fstrm, StoredColors)