├── .gitignore ├── LICENSE ├── README.md ├── demoScatter.png ├── demoTimeseries.png ├── demo_saveFigure.m ├── getFigureSizeScale.m ├── saveFigure.m ├── saveFigureEps.m ├── saveFigureOld.m ├── setFigureSizeScale.m ├── setLineOpacity.m └── setMarkerOpacity.m /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.asv 3 | 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | 341 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SaveFigure: Matlab vector figure export 2 | 3 | SaveFigure is a Matlab utility which provides aesthetically pleasing figure export which provides a few essential features not present in Matlab's built in figure export or any known utility on the FileExchange: 4 | 5 | - instant export to multiple formats, including PDF, SVG, EPS, PNG, while ensuring that all formats look identical 6 | - identical output on multiple platforms (Linux and MacOS currently supported, Windows support should not be difficult to add, contact me if interested) 7 | - preserves alpha blending and transparency on patches, lines, markers, etc. 8 | - sets or preserves nicely rendered fonts (specified in options) 9 | - preserves vector graphics 10 | 11 | I've found the outputs to be more consistent, more faithful to the on-screen displayed figure, and more aesthetically pleasing than other excellent alternatives, including [export_fig](http://www.mathworks.com/matlabcentral/fileexchange/23629-export-fig) by Oliver Woodford and Yair Altman and [savefig](http://www.mathworks.com/matlabcentral/fileexchange/10889-savefig) by Peder Axensten. Please note that this submission includes code copied wholesale from Juerg Schwizer, Oliver Woodford, Yair Altman, and Peder Axensten. 12 | 13 | To achieve faithful, WYSIWYG vector reproduction of Matlab figures, we generate SVGs, then convert to PDF using Inkscape, and then to PNG and EPS if requested using ImageMagick's `convert` utility. I've found that `convert` rarely does a nice job going from SVG to PDF directly, though this could simply be a matter of setting the correct flags. So Matlab is only responsible for generating the SVG. For newer versions of Matlab (R2014a or newer), `saveFigure` uses Matlab's new internal SVG engine, i.e. `print -dsvg`, which faithfully reproduces Matlab figures as SVG. For older versions, the code for generating SVGs is essentially a nice wrapper around [Juerg Schwizer's](http://www.zhinst.com/blogs/schwizer/) plot2svg utility, with a few minor tweaks. The advantage of this approach is that we have complete control of figure output and appearance; the disadvantage is that it requires a complete reconstruction of the figure as an SVG. Consequently, it may not perfectly reproduce the figure in all instances, but it does a fairly decent job. 14 | 15 | # Installation 16 | 17 | SaveFigure requires ImageMagick and inkscape to be installed and accessible from the command line in order to run. The easiest way to accomplish this is to run: 18 | 19 | Linux: 20 | `sudo apt-get install inkscape imagemagick` 21 | 22 | Mac: 23 | `brew cask install inkscape` 24 | 25 | `brew install imagemagick` 26 | 27 | Windows: not working yet, should be doable. If you'd like to help, I'm happy to accept pull requests! 28 | 29 | # Usage 30 | 31 | Save to specific file type: 32 | 33 | `saveFigure('foo.pdf');` 34 | 35 | `saveFigure('foo.png', gcf);` 36 | 37 | Save to set of file types, return full file names: 38 | 39 | `fileNameList = saveFigure('foo', gcf, 'ext', {'pdf', 'png', 'svg', 'fig', 'eps', 'hires.png'});` 40 | 41 | If fileName has no extension, the figure will be saved in multiple formats as specified by the 'ext' parameter value pair. If no 'ext' is specified, the default list is `{'pdf', 'png', 'fig'}`. 42 | 43 | # Demo 44 | 45 | From `demo_saveFigure`: 46 | 47 | ```matlab 48 | % Plot scatter plot with alpha blending 49 | randseed(1); figure(1); clf; 50 | N = 500; dx = randn(N, 1); dy = randn(N, 1); 51 | h = plot(dx, dy, 'o', 'MarkerFaceColor', [0.6 0.6 1], 'LineWidth', 0.1, ... 52 | 'MarkerEdgeColor', 'w', 'MarkerSize', 8); 53 | setMarkerOpacity(h, 0.3, 0.6); 54 | hold on 55 | N = 500; dx = randn(N, 1) + 1; dy = randn(N, 1); 56 | h = plot(dx, dy, 'o', 'MarkerFaceColor', [1 0.6 0.6], 'LineWidth', 0.1, ... 57 | 'MarkerEdgeColor', 'w', 'MarkerSize', 8); 58 | setMarkerOpacity(h, 0.3, 0.6); 59 | 60 | xlabel('Param 1'); ylabel('Param 2'); title('SaveFigure Demo'); 61 | box off; axis equal; 62 | 63 | saveFigure('demoScatter.png') 64 | ``` 65 | 66 | ![](https://github.com/djoshea/matlab-save-figure/blob/master/demoScatter.png) 67 | 68 | ```matlab 69 | % Plot timeseries with translucent error regions 70 | randseed(2); 71 | K = 6; N = 1000; t = (0:N-1) - 100; 72 | y = sgolayfilt(randn(N, K), 3, 99, [], 1); 73 | ye = sgolayfilt(randn(N, K) * 0.5, 3, 99, [], 1); 74 | 75 | figure(2), clf; hold on; 76 | cmap = parula(K); 77 | for k = 1:K 78 | % errorshade defined below 79 | errorshade(t, y(:, k), ye(:, k), cmap(k, :), 'errorAlpha', 0.5, 'lineAlpha', 0.9); 80 | end 81 | 82 | box off; xlim([0 800]); 83 | xlabel('Time'); ylabel('Signal'); title('SaveFigure Demo'); 84 | 85 | saveFigure('demoTimeseries.png'); 86 | ``` 87 | 88 | ![](https://github.com/djoshea/matlab-save-figure/blob/master/demoTimeseries.png) 89 | 90 | # Line and Marker Opacity 91 | 92 | Included in the repo are two utilities `setLineOpacity` and `setMarkerOpacity` which will set the opacity of lines and markers in plots, respectively. 93 | 94 | ```matlab 95 | setLineOpacity(hLine, edgeAlpha) 96 | ``` 97 | 98 | and 99 | 100 | ```matlab 101 | setMarkerOpacity(hLine, markerFaceAlpha, markerEdgeAlpha) 102 | ``` 103 | 104 | In newer versions of MATLAB, this opacity setting will occur directly on the graphics handle and alter the appearance of the Matlab figure. In older versions of Matlab where these opacity settings are not supported, these settings will be stored in the UserHandle of the figure, where saveFigure will search for the setting upon saving. Thus, the opacity will not be visible in Matlab but will be reflected in the saved PDF, PNG, or EPS file. 105 | 106 | # Credit 107 | 108 | saveFigure internally relies heavily on (and includes within it) code from: 109 | 110 | - [plot2svg](http://www.mathworks.com/matlabcentral/fileexchange/7401-scalable-vector-graphics--svg--export-of-figures) by [Juerg Schwizer](http://www.zhinst.com/blogs/schwizer/) 111 | - [copyfig](http://www.mathworks.com/matlabcentral/fileexchange/23629-export-fig) by Oliver Woodford and (Yair Altman)[http://undocumentedmatlab.com] 112 | - [GetFullPath](http://www.mathworks.com/matlabcentral/fileexchange/28249-getfullpath) by Jan Simon 113 | -------------------------------------------------------------------------------- /demoScatter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djoshea/matlab-save-figure/1bc965433359501cd66f9607cbe2d671d0386b28/demoScatter.png -------------------------------------------------------------------------------- /demoTimeseries.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djoshea/matlab-save-figure/1bc965433359501cd66f9607cbe2d671d0386b28/demoTimeseries.png -------------------------------------------------------------------------------- /demo_saveFigure.m: -------------------------------------------------------------------------------- 1 | function demo_saveFigure() 2 | 3 | % Plot scatter plot with alpha blending 4 | randseed(1); figure(1); clf; 5 | N = 500; dx = randn(N, 1); dy = randn(N, 1); 6 | h = plot(dx, dy, 'o', 'MarkerFaceColor', [0.6 0.6 1], 'LineWidth', 0.1, 'MarkerEdgeColor', 'w', 'MarkerSize', 8); 7 | setMarkerOpacity(h, 0.3, 0.3); 8 | hold on 9 | N = 500; dx = randn(N, 1) + 1; dy = randn(N, 1); 10 | h = plot(dx, dy, 'o', 'MarkerFaceColor', [1 0.6 0.6], 'LineWidth', 0.1, 'MarkerEdgeColor', 'w', 'MarkerSize', 8); 11 | setMarkerOpacity(h, 0.3, 0.3); 12 | 13 | xlabel('Param 1'); ylabel('Param 2'); title('SaveFigure Demo'); 14 | box off; axis equal; 15 | 16 | saveFigure('demoScatter.pdf', 'painters', true) 17 | 18 | 19 | % Plot timeseries with translucent error regions 20 | randseed(2); 21 | K = 6; N = 1000; t = (0:N-1) - 100; 22 | y = sgolayfilt(randn(N, K), 3, 99, [], 1); 23 | ye = sgolayfilt(randn(N, K) * 0.5, 3, 99, [], 1); 24 | 25 | figure(2), clf; hold on; 26 | cmap = parula(K); 27 | for k = 1:K 28 | % errorshade defined below 29 | errorshade(t, y(:, k), ye(:, k), cmap(k, :), 'errorAlpha', 0.5, 'lineAlpha', 0.9); 30 | end 31 | 32 | box off; xlim([0 800]); 33 | xlabel('Time'); ylabel('Signal'); title('SaveFigure Demo'); 34 | 35 | saveFigure('demoTimeseries.pdf'); 36 | end 37 | 38 | %% errorshade code 39 | 40 | function [hl, hs] = errorshade(x, ym, ye, color, varargin) 41 | % [ha] = shadeYInterval(x, y1, y2, varargin) 42 | % shadeYInterval draws two lines on a plot and shades the area between those 43 | % lines. ParamValues are same as for fill command (e.g. FaceColor, 44 | % EdgeColor) 45 | % 46 | 47 | p = inputParser(); 48 | p.addParameter('showLine', true, @islogical); 49 | p.addParameter('errorColor', [], @(x) true); 50 | p.addParameter('lineArgs', {}, @iscell); 51 | p.addParameter('shadeArgs', {}, @iscell); 52 | p.addParameter('axh', [], @(x) true); 53 | p.addParameter('lineAlpha', 1, @isscalar); 54 | p.addParameter('errorAlpha', 1, @isscalar); 55 | p.addParameter('z', 0, @isscalar); % used for visual stacking on 2-d plots 56 | p.CaseSensitive = false; 57 | p.parse(varargin{:}); 58 | 59 | z = p.Results.z; 60 | 61 | if isempty(p.Results.axh) 62 | axh = newplot; 63 | else 64 | axh = p.Results.axh; 65 | end 66 | 67 | y1 = ym - ye; 68 | y2 = ym + ye; 69 | 70 | if all(isnan(y1) | isnan(y2)) 71 | hl = NaN; 72 | hs = NaN; 73 | return; 74 | end 75 | 76 | % plot the shaded area 77 | x = makerow(x); 78 | y1 = makerow(y1); 79 | y2 = makerow(y2); 80 | 81 | % desaturate the color for shading if not translucent 82 | if isempty(p.Results.errorColor) 83 | if p.Results.errorAlpha < 1 84 | shadeColor = color; 85 | else 86 | shadeColor = 1 - (1-color)*0.5; 87 | end 88 | else 89 | shadeColor = p.Results.errorColor; 90 | end 91 | 92 | % need to split the vecs 93 | nanMask = isnan(y1) | isnan(y2); 94 | offset = 1; 95 | while(offset < numel(x)) 96 | % find next non-nan sample 97 | newOffset = find(~nanMask(offset:end), 1, 'first'); 98 | if isempty(newOffset) 99 | break; 100 | end 101 | 102 | offset = offset+newOffset-1; 103 | nextNaN = find(nanMask(offset:end), 1, 'first'); 104 | if isempty(nextNaN) 105 | regionEnd = numel(x); 106 | else 107 | regionEnd = nextNaN+offset - 2; 108 | end 109 | 110 | regionStart = offset; 111 | mask = regionStart:regionEnd; 112 | 113 | [hs] = shadeSimple(axh, x(mask), y1(mask), y2(mask), z, 'FaceColor', shadeColor, ... 114 | 'alpha', p.Results.errorAlpha, p.Results.shadeArgs{:}); 115 | 116 | offset = regionEnd + 1; 117 | end 118 | 119 | hold(axh, 'on'); 120 | if p.Results.showLine 121 | if z ~=0 122 | zv = z*ones(size(v)); 123 | hl = plot(x, ym, zv, 'Color', color, 'Parent', axh, p.Results.lineArgs{:}); 124 | else 125 | hl = plot(x, ym, 'Color', color, 'Parent', axh, p.Results.lineArgs{:}); 126 | end 127 | setLineOpacity(hl, p.Results.lineAlpha); 128 | else 129 | hl = NaN; 130 | end 131 | 132 | end 133 | 134 | function [ha] = shadeSimple(axh, x, y1, y2, z, varargin) 135 | 136 | p = inputParser(); 137 | p.addParameter('FaceColor', [0.8 0.8 1], @(x) true); 138 | p.addParameter('EdgeColor', 'none', @(x) true); 139 | p.addParameter('alpha', 1, @isscalar); 140 | p.KeepUnmatched = false; 141 | p.parse(varargin{:}); 142 | 143 | xv = [x, fliplr(x)]; 144 | yv = [y1, fliplr(y2)]; 145 | zv = z * ones(size(xv)); 146 | 147 | ha = patch(xv, yv, zv, 'k', 'Parent', axh); 148 | set(ha, 'FaceColor', p.Results.FaceColor, ... 149 | 'EdgeColor', p.Results.EdgeColor, 'Parent', axh, 'FaceAlpha', p.Results.alpha); 150 | 151 | % hide shading from legend 152 | set(get(get(ha, 'Annotation'), 'LegendInformation'), 'IconDisplayStyle', 'off'); 153 | 154 | set(axh, 'Layer', 'top') 155 | 156 | end 157 | 158 | 159 | function vec = makerow( vec ) 160 | % convert vector to row vector 161 | 162 | % leave size == [1 0] alone too 163 | if(size(vec, 1) > size(vec,2) && isvector(vec)) && ~(size(vec, 2) == 0 && size(vec, 1) == 1) 164 | vec = vec'; 165 | end 166 | 167 | if size(vec, 1) == 0 && size(vec, 2) == 1 168 | vec = vec'; 169 | end 170 | 171 | end 172 | 173 | -------------------------------------------------------------------------------- /getFigureSizeScale.m: -------------------------------------------------------------------------------- 1 | function scale = getFigureSizeScale() 2 | 3 | scale = getenv('FIGURE_SIZE_SCALE'); 4 | if isempty(scale) 5 | scale = 1; 6 | else 7 | scale = str2double(scale); 8 | end 9 | 10 | end 11 | -------------------------------------------------------------------------------- /saveFigure.m: -------------------------------------------------------------------------------- 1 | function fileList = saveFigure(varargin) 2 | % saveFigure('filename.pdf', figh) 3 | % fileList = saveFigure('filename', figh, 'ext', {'pdf', 'fig', 'png', 'eps'}) 4 | % 5 | % Saves a figure to a variety of formats by automatically determining the 6 | % best approach to take. v20160418. Currently, this looks like: 7 | % 8 | % If Matlab thinks the figure is too complex to use the painters renderer, 9 | % then we save as EPS (using opengl renderer) and convert to PDF using epstopdf, 10 | % then we convert to other formats via imagemagick's convert. If the figure is 11 | % simple enough for painters, then we save as SVG using print -dsvg, then convert 12 | % to pdf using inkscape, then to other formats using imagemagick's convert. 13 | % Obviously this is complicated, but I haven't found the other approaches 14 | % to work consistently. 15 | % 16 | % * Images look identical across platforms (Mac OS, Linux) 17 | % * Images look identical across different formats, since all conversion 18 | % uses tools outside of Matlab 19 | % * All fonts in figure can be set to any TrueType / OpenType font 20 | % that is installed on your system and will render correctly 21 | % 22 | % You must install image-magick and inkscape before using, e.g. 23 | % Ubuntu/Debian: sudo apt-get install imagemagick inkscape 24 | % Mac via Homebrew: brew install imagemagick inkscape 25 | % [I would download inkscape on Mac directly, building it takes a while!] 26 | % 27 | % Currently not supported for Windows, though adding this shouldn't be too 28 | % difficult. Mostly just need to get the system() calls to work properly. 29 | % 30 | % If the script cannot find imagemagick's convert, set the environment vars 31 | % 32 | % Required 33 | % 34 | % name : name for figure(s), in one of the following forms: 35 | % string : 36 | % if name has an extension at the end, only that format will be saved 37 | % if name has no extension at the end, extensions in exts will be 38 | % added 39 | % cellstr : each entry corresponds to one extension, names must have 40 | % valid extendsion 41 | % struct : field value .(ext) will be used for each extension ext 42 | % 43 | % Optional: 44 | % 45 | % figh : figure handle, [default=gcf] 46 | % 47 | % Param / Value pairs: 48 | % 49 | % ext : list of extensions to use when name does not have extension already, 50 | % default={'pdf', 'png', 'svg'}. 51 | % Options include: 'fig', 'png', 'svg', 'eps', 'pdf' 52 | % 53 | % quiet : print status messages [default = true] 54 | % 55 | % Usage Examples: 56 | % saveFigure('figureName.pdf'); 57 | % fileList = saveFigure('figureName', gcf, 'ext', {'pdf', 'fig', 'png'}); 58 | % saveFigure('figureName.png', gcf, 'fontName', 'Helvetica'); 59 | % 60 | % Dan O'Shea dan@djoshea.com 61 | % (c) 2014-2015 62 | % 63 | % This code internally relies heavily on: 64 | % plot2svg : Juerg Schwizer [ http://www.zhinst.com/blogs/schwizer/ ] 65 | % copyfig : Oliver Woodford 66 | % GetFullPath: Jan Simon 67 | % 68 | % Recent changes: 69 | % 2020 May 28 : fixing issues with misplaced text when font is changed to sans-serif and text wasn't left aligned 70 | 71 | extListFull = {'fig', 'png', 'svg', 'eps', 'pdf'}; 72 | extListDefault = {'fig', 'pdf', 'png'}; 73 | 74 | p = inputParser; 75 | p.addOptional('name', '', @(x) ischar(x) || isstring(x) || iscellstr(x) || isstruct(x) || isa(x, 'function_handle')); 76 | p.addOptional('figh', gcf, @ishandle); 77 | p.addParameter('ext', {}, @(x) ischar(x) || iscellstr(x) || isstring(x)); 78 | p.addParameter('quiet', true, @islogical); 79 | p.addParameter('notes', '', @ischar); 80 | p.addParameter('resolution', 300, @isscalar); 81 | 82 | % set to override resolution to achieve specific pixel width 83 | p.addParameter('rasterWidthPixels', [], @(x) isempty(x) || isscalar(x)); 84 | 85 | p.addParameter('replaceFonts', isMATLABReleaseOlderThan("R2025a"), @islogical); 86 | 87 | p.addParameter('exportFont', '', @ischar); 88 | 89 | p.addParameter('escapeText', true, @islogical); % escape special characters in hopes of preventing text from being outlined, set to false if there are weird numbers appearing (because text was outlined anyway) 90 | 91 | p.addParameter('painters', [], @(x) isempty(x) || islogical(x)); % set to true to force vector rendering when otherwise not possible 92 | p.addParameter('upsample', 1, @isscalar); % improve the rendering quality by rendering to a larger SVG canvas then downsampling. especially useful for small markers or figure sizes, set this to 5-10 93 | 94 | p.addParameter('transparentBackground', false, @islogical); % requires painters to be true 95 | 96 | p.addParameter('replaceStrokeDashArray_dashed', '', @isstringlike); 97 | p.addParameter('replaceStrokeDashArray_dotted', '', @isstringlike); 98 | p.addParameter('replaceStrokeDashArray_dashdot', '', @isstringlike); 99 | 100 | % p.KeepUnmatched = true; 101 | p.parse(varargin{:}); 102 | hfig = p.Results.figh; 103 | name = p.Results.name; 104 | ext = cellstr(p.Results.ext); 105 | quiet = p.Results.quiet; 106 | resolution = p.Results.resolution; 107 | usePainters = ~isempty(p.Results.painters) && p.Results.painters; 108 | 109 | exportFont = p.Results.exportFont; 110 | if isempty(exportFont) 111 | exportFont = getenv('SAVEFIGURE_EXPORT_FONT'); 112 | end 113 | if isempty(exportFont) 114 | exportFont = get(groot, 'DefaultAxesFontName'); 115 | end 116 | exportFont = strtrim(string(exportFont)); 117 | 118 | % lookup extra fonts to append onto the font-family list for missing glyphs 119 | % separate commas 120 | backupFonts = string(getenv('SAVEFIGURE_FALLBACK_FONTFAMILY')); 121 | if backupFonts == "" 122 | backupFonts = "Helvetica,sans-serif"; 123 | end 124 | exportFontFamily = [exportFont; strtrim(strsplit(backupFonts, ','))']; 125 | 126 | oldInvertHardcopy = hfig.InvertHardcopy; 127 | hfig.InvertHardcopy = 'off'; 128 | 129 | if isempty(name) 130 | name = get(hfig, 'Name'); 131 | end 132 | 133 | scale = getenv('FIGURE_SIZE_SCALE'); 134 | if isempty(scale) 135 | scale = 1; 136 | else 137 | scale = str2double(scale); 138 | end 139 | 140 | patchSvgArgs = {'replaceStrokeDashArray_dashed', p.Results.replaceStrokeDashArray_dashed, ... 141 | 'replaceStrokeDashArray_dotted', p.Results.replaceStrokeDashArray_dotted, ... 142 | 'replaceStrokeDashArray_dashdot', p.Results.replaceStrokeDashArray_dashdot, ... 143 | 'pixelated', usePainters, ... 144 | 'replaceFonts', p.Results.replaceFonts, ... 145 | 'escapeText', p.Results.escapeText}; 146 | 147 | % build a map with .ext = file with ext 148 | fileInfo = containers.Map('KeyType', 'char', 'ValueType', 'char'); 149 | 150 | if isstring(name) 151 | if numel(name) > 1 152 | name = cellstr(name); 153 | else 154 | name = char(name); 155 | end 156 | end 157 | 158 | % parse name and extensions, build fileInfo map 159 | if isstruct(name) 160 | fields = fieldnames(name); 161 | for iF = 1:fields 162 | fileInfo(fields{iF}) = GetFullPath(name.(fields{iF})); 163 | end 164 | 165 | elseif iscell(name) % expect each argument to have extension already 166 | assert(isempty(ext), 'Extension list invalid with cell name argument'); 167 | [extList] = cellfun(@getExtensionsFromFile, name, 'UniformOutput', false); 168 | for iF = 1:length(extList) 169 | if ~ismember(extList{iF}, extListFull) 170 | error('Could not extract valid extension from file name %s', name{iF}); 171 | end 172 | fileInfo(extList{iF}{1}) = GetFullPath(name{iF}); 173 | end 174 | 175 | elseif ischar(name) % may or may not have extension 176 | 177 | [extsFromName, fileWithEachExt] = getExtensionsFromFile(name); 178 | if ~isempty(extsFromName) && any(ismember(extsFromName, extListFull)) 179 | % single file name with extension 180 | assert(isempty(ext), 'Extension list invalid when name argument already has extension'); 181 | 182 | for iE = 1:numel(extsFromName) 183 | fileInfo(extsFromName{iE}) = GetFullPath(fileWithEachExt{iE}); 184 | end 185 | else 186 | % single file name with no extension, use extension list 187 | % (default if not found) 188 | if isempty(ext) 189 | ext = extListDefault; 190 | end 191 | if ~iscell(ext) 192 | ext = {ext}; 193 | end 194 | for iE = 1:numel(ext) 195 | fileInfo(ext{iE}) = GetFullPath(sprintf('%s.%s', name, ext{iE})); 196 | end 197 | end 198 | else 199 | error('Unknown format for argument name'); 200 | end 201 | 202 | values = fileInfo.values; 203 | [pathFinal, nameFinal] = fileparts(values{1}); 204 | 205 | if ~isempty(pathFinal) && exist(pathFinal, 'dir') == 0 206 | mkdirRecursive(pathFinal); 207 | end 208 | 209 | % save figure notes 210 | if ~isempty(p.Results.notes) 211 | notes = p.Results.notes; 212 | notesFile = fullfile(pathFinal, [nameFinal, '.notes.txt']); 213 | [fid, msg] = fopen(notesFile, 'w'); 214 | if fid == -1 215 | error('Error opening notes file %s : %s', notesFile, msg); 216 | end 217 | 218 | fprintf(fid, '%s', notes); 219 | fprintf(fid, '\n\nSaved on %s\n\nFile list:\n', datestr(now)); 220 | for iKey = 1:fileInfo.Count 221 | fprintf(fid, '%s\n', values{iKey}); 222 | end 223 | fclose(fid); 224 | end 225 | 226 | % check extensions 227 | extList = fileInfo.keys; 228 | nonSupported = setdiff(extList, extListFull); 229 | if ~isempty(nonSupported) 230 | error('Non-supported extensions %', strjoin(nonSupported, ', ')); 231 | end 232 | 233 | fileList = {}; % files saved 234 | tempList = {}; % temp files saved to be deleted 235 | 236 | % Save fig format 237 | if fileInfo.isKey('fig') 238 | file = fileInfo('fig'); 239 | fileList{end+1} = file; 240 | if ~quiet 241 | printmsg('fig', file); 242 | end 243 | saveas(hfig, file, 'fig'); 244 | end 245 | 246 | % make sure the size of the figure is WYSIWYG 247 | set(hfig, 'PaperUnits' ,'centimeters'); 248 | set(hfig, 'Units', 'centimeters'); 249 | % set(hfig, 'PaperPositionMode', 'auto'); 250 | pos = hfig.Position; 251 | figSizeCm = pos(3:4); 252 | 253 | % adjust size by scale 254 | figSizeCm = figSizeCm / scale; 255 | 256 | try 257 | AutoAxis.disableFigure(); % prevent fonts from being changed mid-save 258 | catch 259 | end 260 | 261 | savedFigLims = figFreezeLims(hfig); 262 | 263 | % prevent font outlining by setting everything to a boring font here 264 | % these will be patched back to default font in patchSvgFile 265 | % if p.Results.preventOutlinedFonts 266 | % figSetFonts(hfig, 'FontName', 'SansSerif'); 267 | % end 268 | 269 | if isMATLABReleaseOlderThan("R2025a") 270 | restoreInfo = figPatchText(hfig, 'escapeText', p.Results.escapeText, 'replaceFonts', p.Results.replaceFonts); 271 | end 272 | 273 | % check the figure complexity and determine which path to take 274 | % if verLessThan('matlab', '9.4.0') 275 | % checker = matlab.graphics.internal.PrintVertexChecker.getInstance(); 276 | % exceedsLimits = ~checker.exceedsLimits(hfig); %#ok 277 | % else 278 | % checker = matlab.graphics.internal.PrintPaintersChecker.getInstance(); 279 | % exceedsLimits = ~checker.exceedsVertexLimits(hfig); 280 | % end 281 | 282 | % change normalized units to data units when possible 283 | % normalized units get messed up when upsampling 284 | axh = findall(hfig, 'Type', 'Axes'); 285 | objNormalizedUnits = findall(axh, 'Units', 'normalized', '-not', 'Type', 'Axes', '-not', 'Type', 'arrowshape'); 286 | set(objNormalizedUnits, 'Units', 'data'); 287 | 288 | % trick Matlab into rendering everything at higher resolution 289 | jc = findobjinternal(hfig, '-isa', 'matlab.graphics.primitive.canvas.JavaCanvas', '-depth', 1); 290 | if isa(jc, 'matlab.graphics.GraphicsPlaceholder') 291 | origDPI = str2double(getenv("SCREEN_DPI")); 292 | if isnan(origDPI) 293 | warning('Could not determine screen DPI: JavaCanvas not found and SCREEN_DPI not set'); 294 | origDPI = 72; 295 | end 296 | renderDPI = origDPI * p.Results.upsample; %#ok 297 | else 298 | origDPI = jc.ScreenPixelsPerInch; 299 | if p.Results.upsample > 1 300 | renderDPI = origDPI * p.Results.upsample; 301 | if ~isempty(jc) 302 | % jc.OpenGL = 'off'; 303 | jc.ScreenPixelsPerInch = renderDPI; 304 | if exist('AutoAxis', 'class') 305 | AutoAxis.updateFigure(); 306 | end 307 | end 308 | else 309 | renderDPI = origDPI; %#ok 310 | end 311 | end 312 | 313 | % start with svg format, convert to pdf, then to other formats 314 | needPdf = any(ismember(setdiff(extList, {'fig', 'svg'}), extList)); 315 | needSvg = needPdf || any(ismember(extList, 'svg')); 316 | 317 | % create transparent background if requested 318 | if p.Results.transparentBackground 319 | assert(usePainters, 'Transparent background requires painters parameter to be set to true'); 320 | 321 | % find all axes 322 | hax = findobj(hfig, '-isa', 'matlab.graphics.axis.Axes'); 323 | 324 | setBackground.h = [hfig; hax]; 325 | setBackground.origColor = arrayfun(@(h) h.Color, setBackground.h, 'UniformOutput', false); 326 | set(setBackground.h, 'Color', 'none'); 327 | end 328 | 329 | % save svg format first 330 | if needSvg 331 | if fileInfo.isKey('svg') 332 | % use actual file name 333 | file = fileInfo('svg'); 334 | fileList{end+1} = file; 335 | if ~quiet 336 | printmsg('svg', file); 337 | end 338 | else 339 | % use a temp file name 340 | file = [tempname '.svg']; 341 | tempList{end+1} = file; 342 | end 343 | 344 | svgFile = file; 345 | 346 | % use Matlab's built in svg engine (from Batik Graphics2D for java) 347 | set(hfig,'Units','pixels'); % All data in the svg-file is saved in pixels 348 | % set(hfig, 'Position', round(get(hfig, 'Position'))); 349 | % we specify the resolution because complicated figures will 350 | % save as an image, though we shouldn't get here 351 | 352 | drawnow; 353 | 354 | % force painters renderer if requested 355 | if usePainters 356 | rendArgs = {'-painters'}; 357 | resArgs = {}; 358 | else 359 | rendArgs = {}; 360 | resArgs = {sprintf('-r%.0f', resolution)}; 361 | end 362 | print(hfig, rendArgs{:}, '-dsvg', resArgs{:}, file); 363 | 364 | % now we have to change the svg header to match the size that 365 | % we want the output to be because Inkscape doesn't determine 366 | % this correctly 367 | widthStr = sprintf('%.3fcm', figSizeCm(1)); 368 | heightStr = sprintf('%.3fcm', figSizeCm(2)); 369 | 370 | % patchSvgFile(svgFile, widthStr, heightStr, renderDPI / origDPI, p.Results.exportFont); 371 | patchSvgFile(svgFile, widthStr, heightStr, 1, exportFontFamily, patchSvgArgs{:}); 372 | end 373 | 374 | if needPdf 375 | if fileInfo.isKey('pdf') 376 | % use actual file name 377 | file = fileInfo('pdf'); 378 | fileList{end+1} = file; 379 | if ~quiet 380 | printmsg('pdf', file); 381 | end 382 | else 383 | % use a temp file name 384 | file = [tempname '.pdf']; 385 | tempList{end+1} = file; 386 | end 387 | 388 | % convert to pdf using inkscape 389 | convertSvgToPdf(svgFile, file); 390 | 391 | pdfFile = file; 392 | end 393 | 394 | % cleanup 395 | if ~isempty(jc) 396 | jc.ScreenPixelsPerInch = origDPI; 397 | end 398 | set(objNormalizedUnits, 'Units', 'normalized'); 399 | 400 | 401 | if isMATLABReleaseOlderThan("R2025a") 402 | figRestoreText(restoreInfo); 403 | end 404 | figUnfreezeLims(hfig, savedFigLims); 405 | 406 | hfig.InvertHardcopy = oldInvertHardcopy; 407 | 408 | % restore backgrounds set 409 | if p.Results.transparentBackground 410 | for iH = 1:numel(setBackground.h) 411 | setBackground.h(iH).Color = setBackground.origColor{iH}; 412 | end 413 | end 414 | 415 | % restore AutoAxis 416 | try 417 | AutoAxis.enableFigure(); % we disabled it above 418 | catch 419 | end 420 | 421 | 422 | if fileInfo.isKey('png') 423 | file = fileInfo('png'); 424 | fileList{end+1} = file; 425 | 426 | if ~quiet 427 | printmsg('png', file); 428 | end 429 | 430 | if ~isempty(p.Results.rasterWidthPixels) 431 | % widthPx == figSizeIn * resolution 432 | resolution = p.Results.rasterWidthPixels / (figSizeCm(1) / 2.54); 433 | end 434 | convertPdf(pdfFile, file, resolution); 435 | end 436 | 437 | if fileInfo.isKey('eps') 438 | file = fileInfo('eps'); 439 | fileList{end+1} = file; 440 | if ~quiet 441 | printmsg('eps', file); 442 | end 443 | 444 | convertPdf(pdfFile, file); 445 | end 446 | 447 | % delete temporary files 448 | for tempFile = tempList 449 | delete(tempFile{1}); 450 | end 451 | 452 | fileList = makecol(fileList); 453 | 454 | end 455 | 456 | function patchSvgFile(svgFile, widthStr, heightStr, scaleViewBoxBy, fontName, varargin) 457 | p = inputParser(); 458 | p.addParameter('replaceFonts', true, @islogical); 459 | p.addParameter('escapeText', true, @islogical); 460 | p.addParameter('replaceStrokeDashArray_dashed', '', @isstringlike); 461 | p.addParameter('replaceStrokeDashArray_dotted', '', @isstringlike); 462 | p.addParameter('replaceStrokeDashArray_dashdot', '', @isstringlike); 463 | p.addParameter('pixelated', true, @islogical); 464 | p.parse(varargin{:}); 465 | replaceStrokeDashArray_dashed = string(p.Results.replaceStrokeDashArray_dashed); 466 | replaceStrokeDashArray_dotted = string(p.Results.replaceStrokeDashArray_dotted); 467 | replaceStrokeDashArray_dashdot = string(p.Results.replaceStrokeDashArray_dashdot); 468 | 469 | % 1. replaces first width="..." and height="..." and adds a viewbox to size 470 | % the SVG file appropriately for Inkscape processing 471 | % 2. adds a small stroke to the outside of patch objects to hide white 472 | % lines 473 | 474 | str = fileread(svgFile); 475 | 476 | % first we need to know the current size 477 | 478 | if isMATLABReleaseOlderThan("R2025a") % R2025a 479 | tokens = regexp(str, 'width="(\d+)"', 'tokens', 'once'); 480 | assert(~isempty(tokens), 'Could not find width in SVG file'); 481 | widthPx = str2double(tokens{1}); 482 | tokens = regexp(str, 'height="(\d+)"', 'tokens', 'once'); 483 | assert(~isempty(tokens), 'Could not find height in SVG file'); 484 | heightPx = str2double(tokens{1}); 485 | 486 | viewBoxStr = sprintf('viewBox="0 0 %g %g"', widthPx * scaleViewBoxBy, heightPx * scaleViewBoxBy); 487 | 488 | str = regexprep(str, 'width="\d+"', sprintf('width="%s"', widthStr), 'once'); 489 | str = regexprep(str, 'height="\d+"', sprintf('height="%s" %s', heightStr, viewBoxStr), 'once'); 490 | else 491 | % PDF has width, height as mm and viewbox specified correctly 492 | str = regexprep(str, 'width="([0-9]*\.[0-9]+)mm"', sprintf('width="%s"', widthStr), 'once'); 493 | str = regexprep(str, 'height="([0-9]*\.[0-9]+)mm"', sprintf('height="%s"', heightStr), 'once'); 494 | end 495 | 496 | replaceFonts = p.Results.replaceFonts; 497 | escapeText = p.Results.escapeText; 498 | 499 | if replaceFonts 500 | fontName = string(fontName); 501 | fontFamilyString = ""; 502 | for iF = 1:numel(fontName) 503 | fontFamilyString = fontFamilyString + "'" + fontName(iF) + "'"; 504 | if iF < numel(fontName) 505 | fontFamilyString = fontFamilyString + ", "; 506 | end 507 | end 508 | 509 | % replace SansSerif and Dialog with Helvetica 510 | str = regexprep(str, 'font-family:''Dialog''', sprintf('font-family: %s', fontFamilyString)); 511 | str = regexprep(str, 'font-family:''SansSerif''', sprintf('font-family: %s', fontFamilyString)); 512 | str = regexprep(str, 'font-family:sans-serif', sprintf('font-family: %s', fontFamilyString)); 513 | end 514 | 515 | % add a small stroke to paths with no stroke to hide rendering 516 | % issues 517 | str = regexprep(str, '(]*>)', '$1image-rendering: pixelated !important; $2'); 522 | end 523 | 524 | % replace stroke-dasharray lines corresponding to dashed lines, assumed to be 10,6 525 | if replaceStrokeDashArray_dashed ~= "" 526 | str = regexprep(str, 'stroke-dasharray:10,6', sprintf('stroke-dasharray:%s', replaceStrokeDashArray_dashed)); 527 | end 528 | 529 | % replace stroke-dasharray lines corresponding to dashed lines, typically 1,3 530 | if replaceStrokeDashArray_dotted ~= "" 531 | str = regexprep(str, 'stroke-dasharray:1,3', sprintf('stroke-dasharray:%s', replaceStrokeDashArray_dotted)); 532 | end 533 | 534 | % replace stroke-dasharray lines corresponding to dashed lines, typically 8,3,2,3 535 | if replaceStrokeDashArray_dashdot ~= "" 536 | str = regexprep(str, 'stroke-dasharray:8,3,2,3', sprintf('stroke-dasharray:%s', replaceStrokeDashArray_dashdot)); 537 | end 538 | 539 | if escapeText 540 | %replace escaped unicode codepoints of the form SUB####SUB where SUB is char(26) and #### is the numeric codepoint 541 | % replace with an html escape sequence 542 | SUB = char(26); 543 | str = regexprep(str, [SUB, '(\d+)', SUB], '&#$1;'); 544 | str = regexprep(str, SUB, ''); % needed because there may be a tag containing a SUB character 545 | end 546 | 547 | fid = fopen(svgFile, 'w'); 548 | fprintf(fid, '%s', str); 549 | fclose(fid); 550 | end 551 | 552 | function convertSvgToPdf(svgFile, pdfFile) 553 | % use Inkscape to convert pdf 554 | % if ismac 555 | % inkscapePath = '/usr/local/bin/inkscape'; 556 | % if ~exist(inkscapePath, 'file') 557 | % error('Could not locate Inkscape at %s', inkscapePath); 558 | % end 559 | % else 560 | inkscapePath = getenv('INKSCAPE_PATH'); 561 | if isempty(inkscapePath) 562 | inkscapePath = 'inkscape'; 563 | end 564 | % end 565 | 566 | % MATLAB has it's own older version of libtiff.so inside it, so we 567 | % clear that path when calling imageMagick to avoid issues 568 | if ispc() 569 | lib_prefix = ''; 570 | else 571 | lib_prefix = 'export LANG=en_US.UTF-8; export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; export LD_PRELOAD=""; '; 572 | end 573 | 574 | cmd = sprintf('%s%s --export-filename=%s %s', ... 575 | lib_prefix, escapePathForShell(inkscapePath), escapePathForShell(pdfFile), escapePathForShell(svgFile)); 576 | %cmd = sprintf('%s --export-pdf %s %s', inkscapePath, escapePathForShell(pdfFile), escapePathForShell(svgFile)); 577 | [status, result] = system(cmd); 578 | 579 | if status 580 | fprintf('Error converting svg file. Is Inkscape configured correctly?\n'); 581 | fprintf('%s\n', result); 582 | fprintf('Command was:\n%s\n\n', cmd); 583 | end 584 | end 585 | 586 | % function convertEpsToPdf(epsFile, pdfFile) 587 | % % use Inkscape to convert pdf 588 | % if ismac 589 | % epsToPdfPath = '/Library/TeX/texbin/epstopdf'; 590 | % else 591 | % epsToPdfPath = 'epstopdf'; 592 | % end 593 | % 594 | % % MATLAB has it's own older version of libtiff.so inside it, so we 595 | % % clear that path when calling imageMagick to avoid issues 596 | % cmd = sprintf('export LANG=en_US.UTF-8; export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; %s --res=%d -o=%s %s', ... 597 | % epsToPdfPath, resolution, escapePathForShell(pdfFile), escapePathForShell(epsFile)); 598 | % %cmd = sprintf('%s --export-pdf %s %s', inkscapePath, escapePathForShell(pdfFile), escapePathForShell(svgFile)); 599 | % [status, result] = system(cmd); 600 | % 601 | % if status 602 | % fprintf('Error converting svg file. Is Inkscape configured correctly?\n'); 603 | % fprintf('%s\n', result); 604 | % fprintf('Command was:\n%s\n\n', cmd); 605 | % end 606 | % end 607 | 608 | function convertPdf(pdfFile, file, resolution) 609 | % call imageMagick convert on pdfFile --> file 610 | 611 | % if ismac 612 | % convertPath = '/usr/local/bin/convert'; 613 | % if ~exist(convertPath, 'file') 614 | % error('Could not locate convert at %s', convertPath); 615 | % end 616 | % else 617 | % convertPath = 'convert'; 618 | % end 619 | 620 | % magickPath = getenv('IMAGEMAGICK_MAGICK_PATH'); 621 | % if isempty(magickPath) 622 | % magickPath = 'magick'; 623 | % end 624 | convertPath = getenv('IMAGEMAGICK_CONVERT_PATH'); 625 | if isempty(convertPath) 626 | convertPath = 'convert'; 627 | end 628 | 629 | 630 | % MATLAB has it's own older version of libtiff.so inside it, so we 631 | % clear that path when calling imageMagick to avoid issues 632 | % cmd = sprintf('export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; convert -verbose -quality 100 -density %d %s -resize %d%% %s', ... 633 | % density, escapePathForShell(pdfFile), resize, escapePathForShell(file)); 634 | 635 | if ispc() 636 | lib_prefix = ''; 637 | else 638 | lib_prefix = 'export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; '; 639 | end 640 | % cmd = sprintf('%s%s convert -verbose -density %d %s -resample %d %s', ... 641 | % lib_prefix, escapePathForShell(magickPath), resolution, escapePathForShell(pdfFile), resolution, escapePathForShell(file)); 642 | cmd = sprintf('%s%s -verbose -density %d %s -resample %d %s', ... 643 | lib_prefix, escapePathForShell(convertPath), resolution, escapePathForShell(pdfFile), resolution, escapePathForShell(file)); 644 | [status, result] = system(cmd); 645 | 646 | if status 647 | fprintf('Error converting pdf file. Are ImageMagick and Ghostscript installed?\n'); 648 | fprintf('%s', result); 649 | fprintf('Command was:\n%s\n\n', cmd); 650 | end 651 | end 652 | 653 | function printmsg(ex, file) 654 | fprintf('Saving %s as %s\n', ex, file); 655 | end 656 | 657 | function [exts, fileWithEachExt] = getExtensionsFromFile(file) 658 | [fPath, fName, dotext] = fileparts(file); 659 | if ~isempty(dotext) 660 | % split on + and strip leading dots 661 | exts = strsplit(dotext, '+'); 662 | fileWithEachExt = cell(numel(exts), 1); 663 | for iE = 1:numel(exts) 664 | if exts{iE}(1) == '.' 665 | exts{iE} = exts{iE}(2:end); 666 | end 667 | fileWithEachExt{iE} = fullfile(fPath, [fName '.' exts{iE}]); 668 | end 669 | else 670 | exts = {}; 671 | fileWithEachExt = {}; 672 | end 673 | end 674 | 675 | function str = strjoin(strCell, join) 676 | % str = strjoin(strCell, join) 677 | % creates a string by concatenating the elements of strCell, separated by the string 678 | % in join (default = ', ') 679 | % 680 | % e.g. strCell = {'a','b'}, join = ', ' [ default ] --> str = 'a, b' 681 | 682 | if nargin < 2 683 | join = ', '; 684 | end 685 | 686 | if isempty(strCell) 687 | str = ''; 688 | else 689 | if isnumeric(strCell) || islogical(strCell) 690 | % convert numeric vectors to strings 691 | strCell = arrayfun(@num2str, strCell, 'UniformOutput', false); 692 | elseif iscell(strCell) 693 | strCell = cellfun(@num2str, strCell, 'UniformOutput', false); 694 | end 695 | 696 | strCell = cellfun(@num2str, strCell, 'UniformOutput', false); 697 | 698 | str = cellfun(@(str) [str join], strCell, ... 699 | 'UniformOutput', false); 700 | str = [str{:}]; 701 | str = str(1:end-length(join)); 702 | end 703 | end 704 | 705 | function path = escapePathForShell(path) 706 | % path = escapePathForShell(path) 707 | % Escape a path to a file or directory for embedding within a shell command 708 | % passed to cmd or unix. 709 | 710 | if ispc() 711 | % quote path 712 | path = ['"', path, '"']; 713 | else 714 | % escape spaces 715 | path = strrep(path, ' ', '\ '); 716 | end 717 | 718 | end 719 | 720 | function File = GetFullPath(File) 721 | % GetFullPath - Get absolute path of a file or folder [MEX] 722 | % FullName = GetFullPath(Name) 723 | % INPUT: 724 | % Name: String or cell string, file or folder name with or without relative 725 | % or absolute path. 726 | % Unicode characters and UNC paths are supported. 727 | % Up to 8192 characters are allowed here, but some functions of the 728 | % operating system may support 260 characters only. 729 | % 730 | % OUTPUT: 731 | % FullName: String or cell string, file or folder name with absolute path. 732 | % "\." and "\.." are processed such that FullName is fully qualified. 733 | % For empty strings the current directory is replied. 734 | % The created path need not exist. 735 | % 736 | % NOTE: The Mex function calls the Windows-API, therefore it does not run 737 | % on MacOS and Linux. 738 | % The magic initial key '\\?\' is inserted on demand to support names 739 | % exceeding MAX_PATH characters as defined by the operating system. 740 | % 741 | % EXAMPLES: 742 | % cd(tempdir); % Here assumed as C:\Temp 743 | % GetFullPath('File.Ext') % ==> 'C:\Temp\File.Ext' 744 | % GetFullPath('..\File.Ext') % ==> 'C:\File.Ext' 745 | % GetFullPath('..\..\File.Ext') % ==> 'C:\File.Ext' 746 | % GetFullPath('.\File.Ext') % ==> 'C:\Temp\File.Ext' 747 | % GetFullPath('*.txt') % ==> 'C:\Temp\*.txt' 748 | % GetFullPath('..') % ==> 'C:\' 749 | % GetFullPath('Folder\') % ==> 'C:\Temp\Folder\' 750 | % GetFullPath('D:\A\..\B') % ==> 'D:\B' 751 | % GetFullPath('\\Server\Folder\Sub\..\File.ext') 752 | % % ==> '\\Server\Folder\File.ext' 753 | % GetFullPath({'..', 'new'}) % ==> {'C:\', 'C:\Temp\new'} 754 | % 755 | % COMPILE: See GetFullPath.c 756 | % Run the unit-test uTest_GetFullPath after compiling. 757 | % 758 | % Tested: Matlab 6.5, 7.7, 7.8, 7.13, WinXP/32, Win7/64 759 | % Compiler: LCC 2.4/3.8, OpenWatcom 1.8, BCC 5.5, MSVC 2008 760 | % Author: Jan Simon, Heidelberg, (C) 2010-2011 matlab.THISYEAR(a)nMINUSsimon.de 761 | % 762 | % See also Rel2AbsPath, CD, FULLFILE, FILEPARTS. 763 | 764 | % $JRev: R-x V:023 Sum:BNPK16hXCfpM Date:22-Oct-2011 00:51:51 $ 765 | % $License: BSD (use/copy/change/redistribute on own risk, mention the author) $ 766 | % $UnitTest: uTest_GetFullPath $ 767 | % $File: Tools\GLFile\GetFullPath.m $ 768 | % History: 769 | % 001: 20-Apr-2010 22:28, Successor of Rel2AbsPath. 770 | % 010: 27-Jul-2008 21:59, Consider leading separator in M-version also. 771 | % 011: 24-Jan-2011 12:11, Cell strings, '~File' under linux. 772 | % Check of input types in the M-version. 773 | % 015: 31-Mar-2011 10:48, BUGFIX: Accept [] as input as in the Mex version. 774 | % Thanks to Jiro Doke, who found this bug by running the test function for 775 | % the M-version. 776 | % 020: 18-Oct-2011 00:57, BUGFIX: Linux version created bad results. 777 | % Thanks to Daniel. 778 | 779 | % Initialize: ================================================================== 780 | % Do the work: ================================================================= 781 | 782 | % ############################################# 783 | % ### USE THE MUCH FASTER MEX ON WINDOWS!!! ### 784 | % ############################################# 785 | 786 | % Difference between M- and Mex-version: 787 | % - Mex-version cares about the limit MAX_PATH. 788 | % - Mex does not work under MacOS/Unix. 789 | % - M is remarkably slower. 790 | % - Mex calls Windows system function GetFullPath and is therefore much more 791 | % stable. 792 | % - Mex is much faster. 793 | 794 | % Disable this warning for the current Matlab session: 795 | % warning off JSimon:GetFullPath:NoMex 796 | % If you use this function e.g. under MacOS and Linux, remove this warning 797 | % completely, because it slows down the function by 40%! 798 | %warning('JSimon:GetFullPath:NoMex', ... 799 | % 'GetFullPath: Using slow M instead of fast Mex.'); 800 | 801 | % To warn once per session enable this and remove the warning above: 802 | %persistent warned 803 | %if isempty(warned) 804 | % warning('JSimon:GetFullPath:NoMex', ... 805 | % 'GetFullPath: Using slow M instead of fast Mex.'); 806 | % warned = true; 807 | % end 808 | 809 | % Handle cell strings: 810 | % NOTE: It is faster to create a function @cell\GetFullPath.m under Linux, 811 | % but under Windows this would shadow the fast C-Mex. 812 | if isa(File, 'cell') 813 | for iC = 1:numel(File) 814 | File{iC} = GetFullPath(File{iC}); 815 | end 816 | return; 817 | end 818 | 819 | isWIN = strncmpi(computer, 'PC', 2); 820 | 821 | % DATAREAD is deprecated in 2011b, but available: 822 | hasDataRead = ([100, 1] * sscanf(version, '%d.%d.', 2) <= 713); 823 | 824 | if isempty(File) % Accept empty matrix as input 825 | if ischar(File) || isnumeric(File) 826 | File = cd; 827 | return; 828 | else 829 | error(['JSimon:', mfilename, ':BadInputType'], ... 830 | ['*** ', mfilename, ': Input must be a string or cell string']); 831 | end 832 | end 833 | 834 | if ischar(File) == 0 % Non-empty inputs must be strings 835 | error(['JSimon:', mfilename, ':BadInputType'], ... 836 | ['*** ', mfilename, ': Input must be a string or cell string']); 837 | end 838 | 839 | if isWIN % Windows: -------------------------------------------------------- 840 | FSep = '\'; 841 | File = strrep(File, '/', FSep); 842 | 843 | isUNC = strncmp(File, '\\', 2); 844 | FileLen = length(File); 845 | if isUNC == 0 % File is not a UNC path 846 | % Leading file separator means relative to current drive or base folder: 847 | ThePath = cd; 848 | if File(1) == FSep 849 | if strncmp(ThePath, '\\', 2) % Current directory is a UNC path 850 | sepInd = strfind(ThePath, '\'); 851 | ThePath = ThePath(1:sepInd(4)); 852 | else 853 | ThePath = ThePath(1:3); % Drive letter only 854 | end 855 | end 856 | 857 | if FileLen < 2 || File(2) ~= ':' % Does not start with drive letter 858 | if ThePath(length(ThePath)) ~= FSep 859 | if File(1) ~= FSep 860 | File = [ThePath, FSep, File]; 861 | else % File starts with separator: 862 | File = [ThePath, File]; 863 | end 864 | else % Current path ends with separator, e.g. "C:\": 865 | if File(1) ~= FSep 866 | File = [ThePath, File]; 867 | else % File starts with separator: 868 | ThePath(length(ThePath)) = []; 869 | File = [ThePath, File]; 870 | end 871 | end 872 | 873 | elseif isWIN && FileLen == 2 && File(2) == ':' % "C:" => "C:\" 874 | % "C:" is the current directory, if "C" is the current disk. But "C:" is 875 | % converted to "C:\", if "C" is not the current disk: 876 | if strncmpi(ThePath, File, 2) 877 | File = ThePath; 878 | else 879 | File = [File, FSep]; 880 | end 881 | end 882 | end 883 | 884 | else % Linux, MacOS: --------------------------------------------------- 885 | FSep = '/'; 886 | File = strrep(File, '\', FSep); 887 | 888 | if strcmp(File, '~') || strncmp(File, '~/', 2) % Home directory: 889 | HomeDir = getenv('HOME'); 890 | if ~isempty(HomeDir) 891 | File(1) = []; 892 | File = [HomeDir, File]; 893 | end 894 | 895 | elseif strncmpi(File, FSep, 1) == 0 896 | % Append relative path to current folder: 897 | ThePath = cd; 898 | if ThePath(length(ThePath)) == FSep 899 | File = [ThePath, File]; 900 | else 901 | File = [ThePath, FSep, File]; 902 | end 903 | end 904 | end 905 | 906 | % Care for "\." and "\.." - no efficient algorithm, but the fast Mex is 907 | % recommended at all! 908 | if contains(File, [FSep, '.']) 909 | if isWIN 910 | if strncmp(File, '\\', 2) % UNC path 911 | index = strfind(File, '\'); 912 | if length(index) < 4 % UNC path without separator after the folder: 913 | return; 914 | end 915 | Drive = File(1:index(4)); 916 | File(1:index(4)) = []; 917 | else 918 | Drive = File(1:3); 919 | File(1:3) = []; 920 | end 921 | else % Unix, MacOS: 922 | isUNC = false; 923 | Drive = FSep; 924 | File(1) = []; 925 | end 926 | 927 | hasTrailFSep = (File(length(File)) == FSep); 928 | if hasTrailFSep 929 | File(length(File)) = []; 930 | end 931 | 932 | if hasDataRead 933 | if isWIN % Need "\\" as separator: 934 | C = dataread('string', File, '%s', 'delimiter', '\\'); %#ok 935 | else 936 | C = dataread('string', File, '%s', 'delimiter', FSep); %#ok 937 | end 938 | else % Use the slower REGEXP in Matlab > 2011b: 939 | C = regexp(File, FSep, 'split'); 940 | end 941 | 942 | % Remove '\.\' directly without side effects: 943 | C(strcmp(C, '.')) = []; 944 | 945 | % Remove '\..' with the parent recursively: 946 | R = 1:length(C); 947 | for dd = reshape(find(strcmp(C, '..')), 1, []) 948 | index = find(R == dd); 949 | R(index) = []; 950 | if index > 1 951 | R(index - 1) = []; 952 | end 953 | end 954 | 955 | if isempty(R) 956 | File = Drive; 957 | if isUNC && ~hasTrailFSep 958 | File(length(File)) = []; 959 | end 960 | 961 | elseif isWIN 962 | % If you have CStr2String, use the faster: 963 | % File = CStr2String(C(R), FSep, hasTrailFSep); 964 | File = sprintf('%s\\', C{R}); 965 | if hasTrailFSep 966 | File = [Drive, File]; 967 | else 968 | File = [Drive, File(1:length(File) - 1)]; 969 | end 970 | 971 | else % Unix: 972 | File = [Drive, sprintf('%s/', C{R})]; 973 | if ~hasTrailFSep 974 | File(length(File)) = []; 975 | end 976 | end 977 | end 978 | 979 | end 980 | 981 | %% Plot2SVG 982 | 983 | function vec = makecol( vec ) 984 | % transpose if it's currently a row vector (unless its 0 x 1, keep as is) 985 | if (size(vec,2) > size(vec, 1) && isvector(vec)) && ~(size(vec, 1) == 0 && size(vec, 2) == 1) 986 | vec = vec'; 987 | end 988 | if size(vec, 1) == 1 && size(vec, 2) == 0 989 | vec = vec'; 990 | end 991 | end 992 | 993 | % function figSetFonts(varargin) 994 | % % figSetFonts(hfig, 'Property', val, ...) or figSetFonts('Property', val, ...) 995 | % % 996 | % % Applies a set of properties to all text objects in figure hfig (defaults 997 | % % to gcf if ommitted). 998 | % % 999 | % % Example: figSetFonts('FontSize', 18); 1000 | % 1001 | % p = inputParser; 1002 | % p.addOptional('hfig', gcf, @ishandle); 1003 | % p.KeepUnmatched = true; 1004 | % p.parse(varargin{:}); 1005 | % hfig = p.Results.hfig; 1006 | % 1007 | % hfont = findobj(hfig, '-property', 'FontName'); 1008 | % set(hfont, p.Unmatched); 1009 | % 1010 | % % handle all the rest (Title, 1011 | % htext = findall(hfig, 'Type', 'Text'); 1012 | % set(htext, p.Unmatched); 1013 | % end 1014 | 1015 | function restoreInfo = figPatchText(varargin) 1016 | % patches text objects to set: 1017 | % - FontName to SansSerif (causes outlined text otherwise) 1018 | % - HorizontalAlignment to left and VerticalAlignment to top, prerving extent (multiline strings are omitted) 1019 | % - Replace unicode codepoints with a placeholder (causes outlined text otherwise) 1020 | 1021 | p = inputParser; 1022 | p.addOptional('hfig', gcf, @ishandle); 1023 | p.addParameter('escapeText', true, @islogical); 1024 | p.addParameter('replaceFonts', true, @islogical) 1025 | p.KeepUnmatched = true; 1026 | p.parse(varargin{:}); 1027 | hfig = p.Results.hfig; 1028 | 1029 | % htext_visible = findobj(hfig, '-property', 'FontName'); 1030 | htext = findall(hfig, '-property', 'FontName'); 1031 | if isempty(htext) 1032 | restoreInfo = []; 1033 | return; 1034 | end 1035 | 1036 | escapeText = p.Results.escapeText; 1037 | replaceFonts = p.Results.replaceFonts; 1038 | 1039 | if ~escapeText && ~replaceFonts 1040 | restoreInfo = []; 1041 | return; 1042 | end 1043 | 1044 | for iH = numel(htext) : -1 : 1 1045 | h = htext(iH); 1046 | restoreInfo{iH}.h = h; 1047 | % isvisible = ismember(h, htext_visible); 1048 | 1049 | % check if xlabel, ylabel, title, subtitle 1050 | istitle = isa(h.Parent, 'matlab.graphics.axis.Axes') && ... 1051 | ( isequal(h, h.Parent.Title) || isequal(h, h.Parent.Subtitle) ); 1052 | 1053 | % only replace the font name and fix position if its visible (excludes title, subtitle) 1054 | if replaceFonts 1055 | restoreInfo{iH}.FontName = h.FontName; 1056 | 1057 | if strcmp(h.Type, 'text') && escapeText 1058 | restoreInfo{iH}.HorizontalAlignment = h.HorizontalAlignment; 1059 | restoreInfo{iH}.VerticalAlignment = h.VerticalAlignment; 1060 | restoreInfo{iH}.Position = h.Position; 1061 | 1062 | old_units = h.Units; 1063 | h.Units = 'normalized'; % so we don't have to worry about reverse axis directions 1064 | 1065 | ext_orig = h.Extent; 1066 | 1067 | h.FontName = 'SansSerif'; % don't change the font until after the extent has been computed 1068 | 1069 | if ~iscell(h.String) % multi-line strings omitted 1070 | h.HorizontalAlignment = 'left'; 1071 | h.VerticalAlignment = 'top'; 1072 | 1073 | % check updated extent and adjust position accordingly 1074 | ext_post = h.Extent; 1075 | pos = h.Position; 1076 | pos(1:2) = pos(1:2) + ext_orig(1:2) - ext_post(1:2); 1077 | h.Position = pos; 1078 | end 1079 | 1080 | h.Units = old_units; 1081 | end 1082 | end 1083 | 1084 | if strcmp(h.Type, 'text') && escapeText 1085 | % replace problematic characters that lead to text being outlined 1086 | % wrap them in SUB (codepoint 26 in utf-8) 1087 | % so codepoint 8357 would become SUB8357SUB where SUB is replaced by the actual char(26) 1088 | strcodes = uint16(char(h.String)); 1089 | if any(strcodes > 255) 1090 | restoreInfo{iH}.String = h.String; 1091 | SUB = uint16(26); 1092 | 1093 | while true 1094 | idx = find(strcodes > 255, 1, 'first'); 1095 | if isempty(idx), break; end 1096 | 1097 | if strcodes(idx) >= 0xD800 % high surrogate character (take 2 characters 1098 | if idx == length(strcodes) 1099 | error('Found high surrogate character in last position of string'); 1100 | end 1101 | high = (strcodes(idx) - 0xD800) * 0x400; 1102 | low = strcodes(idx+1) - 0xDC00; 1103 | code = uint32(high) + uint32(low) + 0x10000; 1104 | strcodes = [strcodes(1:idx-1), SUB, uint16(num2str(code)), SUB, strcodes(idx+2:end)]; 1105 | else 1106 | code = uint16(strcodes(idx)); 1107 | strcodes = [strcodes(1:idx-1), SUB, uint16(num2str(code)), SUB, strcodes(idx+1:end)]; 1108 | end 1109 | end 1110 | 1111 | h.String = char(strcodes); 1112 | end 1113 | elseif replaceFonts 1114 | h.FontName = 'SansSerif'; 1115 | end 1116 | 1117 | end 1118 | 1119 | end 1120 | 1121 | function figRestoreText(restoreInfo) 1122 | for iH = 1:numel(restoreInfo) 1123 | r = restoreInfo{iH}; 1124 | h = r.h; 1125 | 1126 | if isfield(r, 'FontName') 1127 | h.FontName = r.FontName; 1128 | end 1129 | 1130 | if isfield(r, 'HorizontalAlignment') 1131 | h.HorizontalAlignment = r.HorizontalAlignment; 1132 | end 1133 | if isfield(r, 'VerticalAlignment') 1134 | h.VerticalAlignment = r.VerticalAlignment; 1135 | end 1136 | if isfield(r, 'Position') 1137 | h.Position = r.Position; 1138 | end 1139 | if isfield(r, 'String') 1140 | h.String = r.String; 1141 | end 1142 | 1143 | end 1144 | 1145 | end 1146 | 1147 | function saved = figFreezeLims(fig) 1148 | % this code is copied from export_fig: 1149 | % https://github.com/altmany/export_fig: 1150 | % Copyright (C) Oliver Woodford 2008-2014, Yair Altman 2015- 1151 | 1152 | % MATLAB "feature": axes limits and tick marks can change when printing 1153 | Hlims = findall(fig, 'Type', 'axes'); 1154 | % Record the old axes limit and tick modes 1155 | saved.Xlims = make_cell(get(Hlims, 'XLimMode')); 1156 | saved.Ylims = make_cell(get(Hlims, 'YLimMode')); 1157 | saved.Zlims = make_cell(get(Hlims, 'ZLimMode')); 1158 | saved.Xtick = make_cell(get(Hlims, 'XTickMode')); 1159 | saved.Ytick = make_cell(get(Hlims, 'YTickMode')); 1160 | saved.Ztick = make_cell(get(Hlims, 'ZTickMode')); 1161 | saved.Xlabel = make_cell(get(Hlims, 'XTickLabelMode')); 1162 | saved.Ylabel = make_cell(get(Hlims, 'YTickLabelMode')); 1163 | saved.Zlabel = make_cell(get(Hlims, 'ZTickLabelMode')); 1164 | 1165 | % Set all axes limit and tick modes to manual, so the limits and ticks can't change 1166 | % Fix Matlab R2014b bug (issue #34): plot markers are not displayed when ZLimMode='manual' 1167 | set(Hlims, 'XLimMode', 'manual', 'YLimMode', 'manual'); 1168 | set_tick_mode(Hlims, 'X'); 1169 | set_tick_mode(Hlims, 'Y'); 1170 | set(Hlims,'ZLimMode', 'manual'); 1171 | set_tick_mode(Hlims, 'Z'); 1172 | 1173 | saved.Hlims = Hlims; 1174 | 1175 | function A = make_cell(A) 1176 | if ~iscell(A) 1177 | A = {A}; 1178 | end 1179 | end 1180 | 1181 | function set_tick_mode(Hlims, ax) 1182 | % Set the tick mode of linear axes to manual 1183 | % Leave log axes alone as these are tricky 1184 | M = get(Hlims, [ax 'Scale']); 1185 | if ~iscell(M) 1186 | M = {M}; 1187 | end 1188 | M = cellfun(@(c) strcmp(c, 'linear'), M); 1189 | set(Hlims(M), [ax 'TickMode'], 'manual'); 1190 | %set(Hlims(M), [ax 'TickLabelMode'], 'manual'); % this hides exponent label in HG2! 1191 | end 1192 | end 1193 | 1194 | function figUnfreezeLims(fig, saved) %#ok 1195 | Hlims = saved.Hlims; 1196 | 1197 | % Reset the axes limit and tick modes 1198 | for a = 1:numel(Hlims) 1199 | set(Hlims(a), 'XLimMode', saved.Xlims{a}, 'YLimMode', saved.Ylims{a}, 'ZLimMode', saved.Zlims{a},... 1200 | 'XTickMode', saved.Xtick{a}, 'YTickMode', saved.Ytick{a}, 'ZTickMode', saved.Ztick{a},... 1201 | 'XTickLabelMode', saved.Xlabel{a}, 'YTickLabelMode', saved.Ylabel{a}, 'ZTickLabelMode', saved.Zlabel{a}); 1202 | end 1203 | end 1204 | -------------------------------------------------------------------------------- /saveFigureEps.m: -------------------------------------------------------------------------------- 1 | function fileList = saveFigureEps(varargin) 2 | % saveFigure('filename.pdf', figh) 3 | % fileList = saveFigure('filename', figh, 'ext', {'pdf', 'fig', 'png', 'eps'}) 4 | % 5 | % Saves a figure to a variety of formats by first exporting to svg, using 6 | % Jeurg Schwizer's excellent plot2svg utility, and then using ImageMagick 7 | % and Inkscape to convert into PDF, PNG, JPG, etc. This approach has a few 8 | % desirable features: 9 | % * Images look identical across platforms (Mac OS, Linux) 10 | % * Images look identical across different formats, since all conversion 11 | % uses tools outside of Matlab 12 | % * All fonts in figure can be replaced with any TrueType / OpenType font 13 | % that is installed on your system and will render correctly 14 | % 15 | % You must install image-magick and inkscape before using, e.g. 16 | % Ubuntu/Debian: sudo apt-get install imagemagick inkscape 17 | % Mac via Homebrew: brew install imagemagick inkscape 18 | % [I would download inkscape on Mac directly, building it takes a while!] 19 | % 20 | % Currently not supported for Windows, though adding this shouldn't be too 21 | % difficult. Mostly just need to get the system() calls to work properly. 22 | % 23 | % Required 24 | % 25 | % name : name for figure(s), in one of the following forms: 26 | % string : 27 | % if name has an extension at the end, only that format will be saved 28 | % if name has no extension at the end, extensions in exts will be 29 | % added 30 | % cellstr : each entry corresponds to one extension, names must have 31 | % valid extension 32 | % struct : field value .(ext) will be used for each extension ext 33 | % 34 | % Optional: 35 | % 36 | % figh : figure handle, [default=gcf] 37 | % 38 | % Param / Value pairs: 39 | % 40 | % fontName: string of font name to replace all fonts in figure with. 41 | % default = 'Source Sans Pro'. Any font installed on your system may be 42 | % used. 43 | % 44 | % ext : list of extensions to use when name does not have extension already, 45 | % default={'pdf', 'png', 'svg'}. 46 | % Options include: 'fig', 'png', 'hires.png', 'svg', 'eps', 'pdf' 47 | % 48 | % copy : copy the figure before saving to prevent modifications from 49 | % affecting the original figure. [default = true] 50 | % 51 | % quiet : print status messages [default = false] 52 | % 53 | % Usage Examples: 54 | % saveFigure('figureName.pdf'); 55 | % fileList = saveFigure('figureName', gcf, 'ext', {'pdf', 'fig', 'png'}); 56 | % saveFigure('figureName.png', gcf, 'fontName', 'Helvetica'); 57 | % 58 | % Dan O'Shea dan@djoshea.com 59 | % (c) 2014-2015 60 | % 61 | % This code internally relies heavily on: 62 | % plot2svg : Juerg Schwizer [ http://www.zhinst.com/blogs/schwizer/ ] 63 | % copyfig : Oliver Woodford 64 | % GetFullPath: Jan Simon 65 | % 66 | extListFull = {'fig', 'png', 'hires.png', 'svg', 'eps', 'pdf'}; 67 | extListDefault = {'fig', 'pdf', 'png'}; 68 | 69 | p = inputParser; 70 | p.addOptional('name', '', @(x) ischar(x) || iscellstr(x) || isstruct(x) || isa(x, 'function_handle')); 71 | p.addOptional('figh', gcf, @ishandle); 72 | p.addParamValue('fontName', '', @ischar); 73 | p.addParamValue('ext', [], @(x) ischar(x) || iscellstr(x)); 74 | p.addParamValue('quiet', true, @islogical); 75 | p.addParamValue('notes', '', @ischar); 76 | p.addParameter('resolution', 300, @isscalar); 77 | p.addParameter('resolutionHiRes', 600, @isscalar); 78 | % p.KeepUnmatched = true; 79 | p.parse(varargin{:}); 80 | hfig = p.Results.figh; 81 | fontName = p.Results.fontName; 82 | name = p.Results.name; 83 | ext = p.Results.ext; 84 | quiet = p.Results.quiet; 85 | resolution = p.Results.resolution; 86 | resolutionHiRes = p.Results.resolutionHiRes; 87 | 88 | if isempty(name) 89 | name = get(hfig, 'Name'); 90 | end 91 | 92 | % build a map with .ext = file with ext 93 | fileInfo = containers.Map('KeyType', 'char', 'ValueType', 'char'); 94 | 95 | if isstruct(name) 96 | fields = fieldnames(name); 97 | for iF = 1:fields 98 | fileInfo(fields{iF}) = GetFullPath(name.(fields{iF})); 99 | end 100 | 101 | elseif iscell(name) % expect each argument to have extension already 102 | assert(isempty(ext), 'Extension list invalid with cell name argument'); 103 | [extList] = cellfun(@getExtensionFromFile, name, 'UniformOutput', false); 104 | for iF = 1:length(extList) 105 | if ~ismember(extList{iF}, extList) 106 | error('Could not extract valid extension from file name %s', name{iF}); 107 | end 108 | fileInfo(extList{iF}) = GetFullPath(name{iF}); 109 | end 110 | 111 | elseif ischar(name) % may or may not have extension 112 | 113 | extFromName = getExtensionFromFile(name); 114 | if ismember(extFromName, extListFull) 115 | % single file name with extension 116 | assert(isempty(ext), 'Extension list invalid when name argument already has extension'); 117 | fileInfo(extFromName) = GetFullPath(name); 118 | else 119 | % single file name with no extension, use extension list 120 | % (default if not found) 121 | if isempty(ext) 122 | ext = extListDefault; 123 | end 124 | if ~iscell(ext) 125 | ext = {ext}; 126 | end 127 | for iE = 1:numel(ext) 128 | fileInfo(ext{iE}) = GetFullPath(sprintf('%s.%s', name, ext{iE})); 129 | end 130 | end 131 | else 132 | error('Unknown format for argument name'); 133 | end 134 | 135 | values = fileInfo.values; 136 | [pathFinal, nameFinal] = fileparts(values{1}); 137 | 138 | % save figure notes 139 | if ~isempty(p.Results.notes) 140 | notes = p.Results.notes; 141 | notesFile = fullfile(pathFinal, [nameFinal, '.notes.txt']); 142 | [fid, msg] = fopen(notesFile, 'w'); 143 | if fid == -1 144 | error('Error opening notes file %s : %s', notesFile, msg); 145 | end 146 | 147 | fprintf(fid, '%s', notes); 148 | fprintf(fid, '\n\nSaved on %s\n\nFile list:\n', datestr(now)); 149 | for iKey = 1:fileInfo.Count 150 | fprintf(fid, '%s\n', values{iKey}); 151 | end 152 | fclose(fid); 153 | end 154 | 155 | % check extensions 156 | extList = fileInfo.keys; 157 | nonSupported = setdiff(extList, extListFull); 158 | if ~isempty(nonSupported) 159 | error('Non-supported extensions %', strjoin(nonSupported, ', ')); 160 | end 161 | 162 | fileList = {}; 163 | tempList = {}; 164 | 165 | % Save fig format 166 | if fileInfo.isKey('fig') 167 | file = fileInfo('fig'); 168 | fileList{end+1} = file; 169 | if ~quiet 170 | printmsg('fig', file); 171 | end 172 | saveas(hfig, file, 'fig'); 173 | end 174 | 175 | % save svg format if requested 176 | if fileInfo.isKey('svg') 177 | file = fileInfo('svg'); 178 | fileList{end+1} = file; 179 | if ~quiet 180 | printmsg('svg', file); 181 | end 182 | 183 | % set font to Myriad Pro 184 | if ~isempty(fontName) 185 | figSetFont(hfig, 'FontName', fontName); 186 | end 187 | 188 | if verLessThan('matlab', '8.4') 189 | plot2svg(file, hfig); 190 | else 191 | % use Matlab's built in svg engine (from Batik Graphics2D for 192 | % java) 193 | set(hfig,'Units','pixels'); % All data in the svg-file is saved in pixels 194 | % we specify the resolution because complicated figures will 195 | % save as an image 196 | print('-dsvg', sprintf('-r%d', resolution), file); 197 | end 198 | end 199 | 200 | % make sure the size of the figure is WYSIWYG 201 | set(hfig, 'PaperUnits' ,'centimeters'); 202 | set(hfig, 'Units', 'centimeters'); 203 | set(hfig, 'PaperPositionMode', 'auto'); 204 | 205 | % start with eps format, convert to pdf, then to other formats 206 | needPdf = any(ismember(setdiff(extListFull, {'fig', 'svg'}), extList)); 207 | needEps = needPdf || any(ismember(extListFull, 'eps')); 208 | 209 | if fileInfo.isKey('eps') || needEps 210 | if fileInfo.isKey('eps') 211 | % use actual file name 212 | file = fileInfo('eps'); 213 | fileList{end+1} = file; 214 | if ~quiet 215 | printmsg('pdf', file); 216 | end 217 | else 218 | % use a temp file name 219 | file = [tempname '.eps']; 220 | tempList{end+1} = file; 221 | end 222 | if ~quiet 223 | printmsg('eps', file); 224 | end 225 | 226 | epsFile = file; 227 | 228 | print(sprintf('-r%d', resolution), '-depsc2', file); 229 | end 230 | 231 | if fileInfo.isKey('pdf') || needPdf 232 | if fileInfo.isKey('pdf') 233 | % use actual file name 234 | file = fileInfo('pdf'); 235 | fileList{end+1} = file; 236 | if ~quiet 237 | printmsg('pdf', file); 238 | end 239 | else 240 | % use a temp file name 241 | file = [tempname '.pdf']; 242 | tempList{end+1} = file; 243 | end 244 | 245 | % convert to pdf using inkscape 246 | convertEpsToPdf(epsFile, file); 247 | 248 | pdfFile = file; 249 | end 250 | 251 | if fileInfo.isKey('png') 252 | file = fileInfo('png'); 253 | fileList{end+1} = file; 254 | 255 | if ~quiet 256 | printmsg('png', file); 257 | end 258 | 259 | convertPdf(pdfFile, file, false); 260 | end 261 | 262 | if fileInfo.isKey('hires.png') 263 | file = fileInfo('hires.png'); 264 | fileList{end+1} = file; 265 | if ~quiet 266 | printmsg('hires.png', file); 267 | end 268 | 269 | convertPdf(pdfFile, file, true); 270 | end 271 | 272 | % delete temporary files 273 | for tempFile = tempList 274 | delete(tempFile{1}); 275 | end 276 | 277 | fileList = makecol(fileList); 278 | 279 | 280 | function convertEpsToPdf(epsFile, pdfFile) 281 | % use Inkscape to convert pdf 282 | if ismac 283 | epsToPdfPath = '/Library/TeX/texbin/epstopdf'; 284 | else 285 | epsToPdfPath = 'epstopdf'; 286 | end 287 | 288 | % MATLAB has it's own older version of libtiff.so inside it, so we 289 | % clear that path when calling imageMagick to avoid issues 290 | cmd = sprintf('export LANG=en_US.UTF-8; export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; %s --res=%d -o=%s %s', ... 291 | epsToPdfPath, resolution, escapePathForShell(pdfFile), escapePathForShell(epsFile)); 292 | %cmd = sprintf('%s --export-pdf %s %s', inkscapePath, escapePathForShell(pdfFile), escapePathForShell(svgFile)); 293 | [status, result] = system(cmd); 294 | 295 | if status 296 | fprintf('Error converting svg file. Is Inkscape configured correctly?\n'); 297 | fprintf(result); 298 | fprintf('\n'); 299 | end 300 | end 301 | 302 | function convertPdf(pdfFile, file, hires) 303 | % call imageMagick convert on pdfFile --> file 304 | if nargin < 3 305 | hires = false; 306 | end 307 | 308 | % if ismac 309 | % convertPath = '/usr/local/bin/convert'; 310 | % if ~exist(convertPath, 'file') 311 | % error('Could not locate convert at %s', convertPath); 312 | % end 313 | % else 314 | convertPath = 'convert'; 315 | % end 316 | 317 | % MATLAB has it's own older version of libtiff.so inside it, so we 318 | % clear that path when calling imageMagick to avoid issues 319 | % cmd = sprintf('export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; convert -verbose -quality 100 -density %d %s -resize %d%% %s', ... 320 | % density, escapePathForShell(pdfFile), resize, escapePathForShell(file)); 321 | if hires 322 | cmd = sprintf('export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; %s -verbose -density %d %s -resample %d %s', ... 323 | convertPath, resolutionHiRes, escapePathForShell(pdfFile), resolutionHiRes, escapePathForShell(file)); 324 | else 325 | cmd = sprintf('export LD_LIBRARY_PATH=""; export DYLD_LIBRARY_PATH=""; %s -verbose -density %d %s -resample %d %s', ... 326 | convertPath, resolution, escapePathForShell(pdfFile), resolution, escapePathForShell(file)); 327 | end 328 | [status, result] = system(cmd); 329 | 330 | if status 331 | fprintf('Error converting pdf file. Are ImageMagick and Ghostscript installed?\n'); 332 | fprintf(result); 333 | fprintf('\n'); 334 | end 335 | end 336 | 337 | function printmsg(ex, file) 338 | fprintf('Saving %s as %s\n', ex, file); 339 | end 340 | 341 | function figSetFont(hfig, varargin) 342 | % set all fonts in the figure 343 | hfont = findobj(hfig, '-property', 'FontName'); 344 | set(hfont, varargin{:}); 345 | htext = findall(hfig, 'Type', 'Text'); 346 | set(htext, varargin{:}); 347 | drawnow; 348 | end 349 | 350 | function [ext, fileSansExt] = getExtensionFromFile(file) 351 | [fPath, fName, dotext] = fileparts(file); 352 | if ~isempty(dotext) 353 | if strcmp(dotext, '.png') 354 | [~, fName2, ext2] = fileparts(fName); 355 | if strcmp(ext2, '.hires') 356 | ext = 'hires.png'; 357 | fName = fName2; 358 | else 359 | ext = 'png'; 360 | end 361 | else 362 | ext = dotext(2:end); 363 | end 364 | else 365 | ext = ''; 366 | end 367 | fileSansExt = fullfile(fPath, fName); 368 | end 369 | 370 | function str = strjoin(strCell, join) 371 | % str = strjoin(strCell, join) 372 | % creates a string by concatenating the elements of strCell, separated by the string 373 | % in join (default = ', ') 374 | % 375 | % e.g. strCell = {'a','b'}, join = ', ' [ default ] --> str = 'a, b' 376 | 377 | if nargin < 2 378 | join = ', '; 379 | end 380 | 381 | if isempty(strCell) 382 | str = ''; 383 | else 384 | if isnumeric(strCell) || islogical(strCell) 385 | % convert numeric vectors to strings 386 | strCell = arrayfun(@num2str, strCell, 'UniformOutput', false); 387 | elseif iscell(strCell) 388 | strCell = cellfun(@num2str, strCell, 'UniformOutput', false); 389 | end 390 | 391 | strCell = cellfun(@num2str, strCell, 'UniformOutput', false); 392 | 393 | str = cellfun(@(str) [str join], strCell, ... 394 | 'UniformOutput', false); 395 | str = [str{:}]; 396 | str = str(1:end-length(join)); 397 | end 398 | end 399 | 400 | %COPYFIG Create a copy of a figure, without changing the figure 401 | % 402 | % Examples: 403 | % fh_new = copyfig(fh_old) 404 | % 405 | % This function will create a copy of a figure, but not change the figure, 406 | % as copyobj sometimes does, e.g. by changing legends. 407 | % 408 | % IN: 409 | % fh_old - The handle of the figure to be copied. Default: gcf. 410 | % 411 | % OUT: 412 | % fh_new - The handle of the created figure. 413 | 414 | % Copyright (C) Oliver Woodford 2012 415 | function fh = copyfig(fh) 416 | 417 | % Set the default 418 | if nargin == 0 419 | fh = gcf; 420 | end 421 | 422 | hAxes = findobj(fh, 'Type', 'axes'); 423 | nAxes = numel(hAxes); 424 | 425 | props = {'Visible', 'Position', 'Rotation', ... 426 | 'HorizontalAlign', 'VerticalAlign', 'Interpreter'}; 427 | items = {'XLabel', 'YLabel', 'ZLabel', 'Title'}; 428 | savedProps = cell(nAxes, numel(items), numel(props)); 429 | 430 | for iAx = 1:nAxes 431 | axh = hAxes(iAx); 432 | 433 | for iItem = 1:numel(items) 434 | item = get(axh, items{iItem}); 435 | 436 | for iProp = 1:numel(props) 437 | savedProps{iAx, iItem, iProp} = get(item, props{iProp}); 438 | end 439 | end 440 | end 441 | 442 | % Is there a legend? 443 | if isempty(findobj(fh, 'Type', 'axes', 'Tag', 'legend')) 444 | % Safe to copy using copyobj 445 | fh = copyobj(fh, 0); 446 | else 447 | % copyobj will change the figure, so save and then load it instead 448 | tmp_nam = [tempname '.fig']; 449 | hgsave(fh, tmp_nam); 450 | fh = hgload(tmp_nam); 451 | delete(tmp_nam); 452 | end 453 | 454 | hAxes = findobj(fh, 'Type', 'axes'); 455 | for iAx = 1:nAxes 456 | axh = hAxes(iAx); 457 | 458 | for iItem = 1:numel(items) 459 | item = get(axh, items{iItem}); 460 | 461 | for iProp = 1:numel(props) 462 | set(item, props{iProp}, savedProps{iAx, iItem, iProp}); 463 | end 464 | end 465 | end 466 | end 467 | 468 | function path = escapePathForShell(path) 469 | % path = escapePathForShell(path) 470 | % Escape a path to a file or directory for embedding within a shell command 471 | % passed to cmd or unix. 472 | 473 | path = strrep(path, ' ', '\ '); 474 | end 475 | 476 | function File = GetFullPath(File) 477 | % GetFullPath - Get absolute path of a file or folder [MEX] 478 | % FullName = GetFullPath(Name) 479 | % INPUT: 480 | % Name: String or cell string, file or folder name with or without relative 481 | % or absolute path. 482 | % Unicode characters and UNC paths are supported. 483 | % Up to 8192 characters are allowed here, but some functions of the 484 | % operating system may support 260 characters only. 485 | % 486 | % OUTPUT: 487 | % FullName: String or cell string, file or folder name with absolute path. 488 | % "\." and "\.." are processed such that FullName is fully qualified. 489 | % For empty strings the current directory is replied. 490 | % The created path need not exist. 491 | % 492 | % NOTE: The Mex function calls the Windows-API, therefore it does not run 493 | % on MacOS and Linux. 494 | % The magic initial key '\\?\' is inserted on demand to support names 495 | % exceeding MAX_PATH characters as defined by the operating system. 496 | % 497 | % EXAMPLES: 498 | % cd(tempdir); % Here assumed as C:\Temp 499 | % GetFullPath('File.Ext') % ==> 'C:\Temp\File.Ext' 500 | % GetFullPath('..\File.Ext') % ==> 'C:\File.Ext' 501 | % GetFullPath('..\..\File.Ext') % ==> 'C:\File.Ext' 502 | % GetFullPath('.\File.Ext') % ==> 'C:\Temp\File.Ext' 503 | % GetFullPath('*.txt') % ==> 'C:\Temp\*.txt' 504 | % GetFullPath('..') % ==> 'C:\' 505 | % GetFullPath('Folder\') % ==> 'C:\Temp\Folder\' 506 | % GetFullPath('D:\A\..\B') % ==> 'D:\B' 507 | % GetFullPath('\\Server\Folder\Sub\..\File.ext') 508 | % % ==> '\\Server\Folder\File.ext' 509 | % GetFullPath({'..', 'new'}) % ==> {'C:\', 'C:\Temp\new'} 510 | % 511 | % COMPILE: See GetFullPath.c 512 | % Run the unit-test uTest_GetFullPath after compiling. 513 | % 514 | % Tested: Matlab 6.5, 7.7, 7.8, 7.13, WinXP/32, Win7/64 515 | % Compiler: LCC 2.4/3.8, OpenWatcom 1.8, BCC 5.5, MSVC 2008 516 | % Author: Jan Simon, Heidelberg, (C) 2010-2011 matlab.THISYEAR(a)nMINUSsimon.de 517 | % 518 | % See also Rel2AbsPath, CD, FULLFILE, FILEPARTS. 519 | 520 | % $JRev: R-x V:023 Sum:BNPK16hXCfpM Date:22-Oct-2011 00:51:51 $ 521 | % $License: BSD (use/copy/change/redistribute on own risk, mention the author) $ 522 | % $UnitTest: uTest_GetFullPath $ 523 | % $File: Tools\GLFile\GetFullPath.m $ 524 | % History: 525 | % 001: 20-Apr-2010 22:28, Successor of Rel2AbsPath. 526 | % 010: 27-Jul-2008 21:59, Consider leading separator in M-version also. 527 | % 011: 24-Jan-2011 12:11, Cell strings, '~File' under linux. 528 | % Check of input types in the M-version. 529 | % 015: 31-Mar-2011 10:48, BUGFIX: Accept [] as input as in the Mex version. 530 | % Thanks to Jiro Doke, who found this bug by running the test function for 531 | % the M-version. 532 | % 020: 18-Oct-2011 00:57, BUGFIX: Linux version created bad results. 533 | % Thanks to Daniel. 534 | 535 | % Initialize: ================================================================== 536 | % Do the work: ================================================================= 537 | 538 | % ############################################# 539 | % ### USE THE MUCH FASTER MEX ON WINDOWS!!! ### 540 | % ############################################# 541 | 542 | % Difference between M- and Mex-version: 543 | % - Mex-version cares about the limit MAX_PATH. 544 | % - Mex does not work under MacOS/Unix. 545 | % - M is remarkably slower. 546 | % - Mex calls Windows system function GetFullPath and is therefore much more 547 | % stable. 548 | % - Mex is much faster. 549 | 550 | % Disable this warning for the current Matlab session: 551 | % warning off JSimon:GetFullPath:NoMex 552 | % If you use this function e.g. under MacOS and Linux, remove this warning 553 | % completely, because it slows down the function by 40%! 554 | %warning('JSimon:GetFullPath:NoMex', ... 555 | % 'GetFullPath: Using slow M instead of fast Mex.'); 556 | 557 | % To warn once per session enable this and remove the warning above: 558 | %persistent warned 559 | %if isempty(warned) 560 | % warning('JSimon:GetFullPath:NoMex', ... 561 | % 'GetFullPath: Using slow M instead of fast Mex.'); 562 | % warned = true; 563 | % end 564 | 565 | % Handle cell strings: 566 | % NOTE: It is faster to create a function @cell\GetFullPath.m under Linux, 567 | % but under Windows this would shadow the fast C-Mex. 568 | if isa(File, 'cell') 569 | for iC = 1:numel(File) 570 | File{iC} = GetFullPath(File{iC}); 571 | end 572 | return; 573 | end 574 | 575 | isWIN = strncmpi(computer, 'PC', 2); 576 | 577 | % DATAREAD is deprecated in 2011b, but available: 578 | hasDataRead = ([100, 1] * sscanf(version, '%d.%d.', 2) <= 713); 579 | 580 | if isempty(File) % Accept empty matrix as input 581 | if ischar(File) || isnumeric(File) 582 | File = cd; 583 | return; 584 | else 585 | error(['JSimon:', mfilename, ':BadInputType'], ... 586 | ['*** ', mfilename, ': Input must be a string or cell string']); 587 | end 588 | end 589 | 590 | if ischar(File) == 0 % Non-empty inputs must be strings 591 | error(['JSimon:', mfilename, ':BadInputType'], ... 592 | ['*** ', mfilename, ': Input must be a string or cell string']); 593 | end 594 | 595 | if isWIN % Windows: -------------------------------------------------------- 596 | FSep = '\'; 597 | File = strrep(File, '/', FSep); 598 | 599 | isUNC = strncmp(File, '\\', 2); 600 | FileLen = length(File); 601 | if isUNC == 0 % File is not a UNC path 602 | % Leading file separator means relative to current drive or base folder: 603 | ThePath = cd; 604 | if File(1) == FSep 605 | if strncmp(ThePath, '\\', 2) % Current directory is a UNC path 606 | sepInd = strfind(ThePath, '\'); 607 | ThePath = ThePath(1:sepInd(4)); 608 | else 609 | ThePath = ThePath(1:3); % Drive letter only 610 | end 611 | end 612 | 613 | if FileLen < 2 || File(2) ~= ':' % Does not start with drive letter 614 | if ThePath(length(ThePath)) ~= FSep 615 | if File(1) ~= FSep 616 | File = [ThePath, FSep, File]; 617 | else % File starts with separator: 618 | File = [ThePath, File]; 619 | end 620 | else % Current path ends with separator, e.g. "C:\": 621 | if File(1) ~= FSep 622 | File = [ThePath, File]; 623 | else % File starts with separator: 624 | ThePath(length(ThePath)) = []; 625 | File = [ThePath, File]; 626 | end 627 | end 628 | 629 | elseif isWIN && FileLen == 2 && File(2) == ':' % "C:" => "C:\" 630 | % "C:" is the current directory, if "C" is the current disk. But "C:" is 631 | % converted to "C:\", if "C" is not the current disk: 632 | if strncmpi(ThePath, File, 2) 633 | File = ThePath; 634 | else 635 | File = [File, FSep]; 636 | end 637 | end 638 | end 639 | 640 | else % Linux, MacOS: --------------------------------------------------- 641 | FSep = '/'; 642 | File = strrep(File, '\', FSep); 643 | 644 | if strcmp(File, '~') || strncmp(File, '~/', 2) % Home directory: 645 | HomeDir = getenv('HOME'); 646 | if ~isempty(HomeDir) 647 | File(1) = []; 648 | File = [HomeDir, File]; 649 | end 650 | 651 | elseif strncmpi(File, FSep, 1) == 0 652 | % Append relative path to current folder: 653 | ThePath = cd; 654 | if ThePath(length(ThePath)) == FSep 655 | File = [ThePath, File]; 656 | else 657 | File = [ThePath, FSep, File]; 658 | end 659 | end 660 | end 661 | 662 | % Care for "\." and "\.." - no efficient algorithm, but the fast Mex is 663 | % recommended at all! 664 | if ~isempty(strfind(File, [FSep, '.'])) 665 | if isWIN 666 | if strncmp(File, '\\', 2) % UNC path 667 | index = strfind(File, '\'); 668 | if length(index) < 4 % UNC path without separator after the folder: 669 | return; 670 | end 671 | Drive = File(1:index(4)); 672 | File(1:index(4)) = []; 673 | else 674 | Drive = File(1:3); 675 | File(1:3) = []; 676 | end 677 | else % Unix, MacOS: 678 | isUNC = false; 679 | Drive = FSep; 680 | File(1) = []; 681 | end 682 | 683 | hasTrailFSep = (File(length(File)) == FSep); 684 | if hasTrailFSep 685 | File(length(File)) = []; 686 | end 687 | 688 | if hasDataRead 689 | if isWIN % Need "\\" as separator: 690 | C = dataread('string', File, '%s', 'delimiter', '\\'); %#ok 691 | else 692 | C = dataread('string', File, '%s', 'delimiter', FSep); %#ok 693 | end 694 | else % Use the slower REGEXP in Matlab > 2011b: 695 | C = regexp(File, FSep, 'split'); 696 | end 697 | 698 | % Remove '\.\' directly without side effects: 699 | C(strcmp(C, '.')) = []; 700 | 701 | % Remove '\..' with the parent recursively: 702 | R = 1:length(C); 703 | for dd = reshape(find(strcmp(C, '..')), 1, []) 704 | index = find(R == dd); 705 | R(index) = []; 706 | if index > 1 707 | R(index - 1) = []; 708 | end 709 | end 710 | 711 | if isempty(R) 712 | File = Drive; 713 | if isUNC && ~hasTrailFSep 714 | File(length(File)) = []; 715 | end 716 | 717 | elseif isWIN 718 | % If you have CStr2String, use the faster: 719 | % File = CStr2String(C(R), FSep, hasTrailFSep); 720 | File = sprintf('%s\\', C{R}); 721 | if hasTrailFSep 722 | File = [Drive, File]; 723 | else 724 | File = [Drive, File(1:length(File) - 1)]; 725 | end 726 | 727 | else % Unix: 728 | File = [Drive, sprintf('%s/', C{R})]; 729 | if ~hasTrailFSep 730 | File(length(File)) = []; 731 | end 732 | end 733 | end 734 | 735 | end 736 | 737 | %% Plot2SVG 738 | 739 | function vec = makecol( vec ) 740 | % transpose if it's currently a row vector (unless its 0 x 1, keep as is) 741 | if (size(vec,2) > size(vec, 1) && isvector(vec)) && ~(size(vec, 1) == 0 && size(vec, 2) == 1) 742 | vec = vec'; 743 | end 744 | if size(vec, 1) == 1 && size(vec, 2) == 0 745 | vec = vec'; 746 | end 747 | end 748 | 749 | end 750 | 751 | 752 | -------------------------------------------------------------------------------- /saveFigureOld.m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/djoshea/matlab-save-figure/1bc965433359501cd66f9607cbe2d671d0386b28/saveFigureOld.m -------------------------------------------------------------------------------- /setFigureSizeScale.m: -------------------------------------------------------------------------------- 1 | function setFigureSizeScale(scale) 2 | 3 | setenv('FIGURE_SIZE_SCALE', num2str(scale)); 4 | 5 | end 6 | -------------------------------------------------------------------------------- /setLineOpacity.m: -------------------------------------------------------------------------------- 1 | function setLineOpacity(s, alpha) 2 | % setLineOpacity(hLine, alpha) 3 | 4 | % [edge, face] = deal(cell(numel(s), 1)); 5 | for i = 1:numel(s) 6 | 7 | % tag it as translucent for saveFigure to 8 | % pick up during SVG authoring 9 | % userdata = get(s(i),'UserData'); 10 | % userdata.svg.LineAlpha = alpha; 11 | % set(s(i),'UserData', userdata); 12 | 13 | if ~verLessThan('matlab', '8.4') 14 | % % first cache marker opacity 15 | % if isempty(s(i).MarkerHandle) && ~isa(s(i).MarkerHandle, 'matlab.graphics.GraphicsPlaceholder') 16 | % edge{i} = s(i).MarkerHandle.EdgeColorData; 17 | % face{i} = s(i).MarkerHandle.FaceColorData; 18 | % end 19 | 20 | % use RGBA color specification 21 | s(i).Color(4) = alpha; 22 | 23 | % % keep transparent 24 | % addlistener(s(i),'MarkedClean',... 25 | % @(ObjH, EventData) keepAlpha(ObjH, EventData, faceAlpha, edgeAlpha)); 26 | end 27 | end 28 | 29 | % drawnow; 30 | % 31 | % for i = 1:length(s) 32 | % if ~isempty(edge{i}) 33 | % s(i).MarkerHandle.EdgeColorData = edge; 34 | % s(i).MarkerHandle.FaceColorData = face; 35 | % end 36 | % end 37 | 38 | end 39 | % 40 | % function keepAlpha(src, ~, faceAlpha, edgeAlpha) 41 | % mh = src.MarkerHandle; 42 | % if ~isempty(mh.EdgeColorData) 43 | % mh.EdgeColorType = 'truecoloralpha'; 44 | % mh.EdgeColorData(4) = uint8(edgeAlpha*255); 45 | % end 46 | % if ~isempty(mh.FaceColorData) 47 | % mh.FaceColorType = 'truecoloralpha'; 48 | % mh.FaceColorData(4) = uint8(faceAlpha*255); 49 | % end 50 | % end 51 | % 52 | -------------------------------------------------------------------------------- /setMarkerOpacity.m: -------------------------------------------------------------------------------- 1 | function setMarkerOpacity(s, faceAlpha, edgeAlpha) 2 | % stores information in UserData struct to cause saveFigure to render 3 | % marker points as translucent when exporting to svg 4 | if nargin < 3 5 | edgeAlpha = faceAlpha; 6 | end 7 | 8 | for i = 1:length(s) 9 | % old version, simply tag it as translucent for saveFigure to pick 10 | % up during SVG authoring 11 | 12 | % userdata = get(s(i),'UserData'); 13 | % userdata.svg.MarkerFaceAlpha = faceAlpha; 14 | % userdata.svg.MarkerEdgeAlpha = edgeAlpha; 15 | % set(s(i),'UserData', userdata); 16 | 17 | if ~verLessThan('matlab', '8.4') 18 | mh = s.MarkerHandle; 19 | if ~isa(mh, 'matlab.graphics.GraphicsPlaceholder') 20 | % do update now if we can, otherwise wait for MarkedClean 21 | mh = s.MarkerHandle; 22 | 23 | if ~isempty(mh.EdgeColorData) 24 | mh.EdgeColorType = 'truecoloralpha'; 25 | mh.EdgeColorData(4) = uint8(edgeAlpha*255); 26 | end 27 | if ~isempty(mh.FaceColorData) 28 | mh.FaceColorType = 'truecoloralpha'; 29 | mh.FaceColorData(4) = uint8(faceAlpha*255); 30 | end 31 | end 32 | 33 | % keep transparent on each MarkedClean, otherwise gets lost 34 | addlistener(s(i),'MarkedClean',... 35 | @(ObjH, EventData) keepAlpha(ObjH, EventData, faceAlpha, edgeAlpha)); 36 | end 37 | end 38 | end 39 | 40 | function keepAlpha(src, ~, faceAlpha, edgeAlpha) 41 | mh = src.MarkerHandle; 42 | if ~isempty(mh.EdgeColorData) 43 | mh.EdgeColorType = 'truecoloralpha'; 44 | mh.EdgeColorData(4) = uint8(edgeAlpha*255); 45 | end 46 | if ~isempty(mh.FaceColorData) 47 | mh.FaceColorType = 'truecoloralpha'; 48 | mh.FaceColorData(4) = uint8(faceAlpha*255); 49 | end 50 | end 51 | 52 | --------------------------------------------------------------------------------