You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

PPTX2PNG.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427
  1. /*
  2. * ====================================================================
  3. * Licensed to the Apache Software Foundation (ASF) under one or more
  4. * contributor license agreements. See the NOTICE file distributed with
  5. * this work for additional information regarding copyright ownership.
  6. * The ASF licenses this file to You under the Apache License, Version 2.0
  7. * (the "License"); you may not use this file except in compliance with
  8. * the License. You may obtain a copy of the License at
  9. *
  10. * http://www.apache.org/licenses/LICENSE-2.0
  11. *
  12. * Unless required by applicable law or agreed to in writing, software
  13. * distributed under the License is distributed on an "AS IS" BASIS,
  14. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  15. * See the License for the specific language governing permissions and
  16. * limitations under the License.
  17. * ====================================================================
  18. */
  19. package org.apache.poi.xslf.util;
  20. import java.awt.AlphaComposite;
  21. import java.awt.Graphics2D;
  22. import java.awt.RenderingHints;
  23. import java.awt.geom.Dimension2D;
  24. import java.io.File;
  25. import java.io.FileOutputStream;
  26. import java.io.IOException;
  27. import java.io.InputStream;
  28. import java.nio.charset.Charset;
  29. import java.util.Locale;
  30. import java.util.Set;
  31. import java.util.regex.Pattern;
  32. import org.apache.poi.common.usermodel.GenericRecord;
  33. import org.apache.poi.poifs.filesystem.FileMagic;
  34. import org.apache.poi.sl.draw.Drawable;
  35. import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
  36. import org.apache.poi.util.Dimension2DDouble;
  37. import org.apache.poi.util.GenericRecordJsonWriter;
  38. import org.apache.poi.util.LocaleUtil;
  39. import org.apache.poi.xslf.util.OutputFormat.BitmapFormat;
  40. import org.apache.poi.xslf.util.OutputFormat.SVGFormat;
  41. /**
  42. * An utility to convert slides of a .pptx slide show to a PNG image
  43. */
  44. public final class PPTX2PNG {
  45. private static final String INPUT_PAT_REGEX =
  46. "(?<slideno>[^|]+)\\|(?<format>[^|]+)\\|(?<basename>.+)\\.(?<ext>[^.]++)";
  47. private static final Pattern INPUT_PATTERN = Pattern.compile(INPUT_PAT_REGEX);
  48. private static final String OUTPUT_PAT_REGEX = "${basename}-${slideno}.${format}";
  49. private static void usage(String error){
  50. String msg =
  51. "Usage: PPTX2PNG [options] <.ppt/.pptx/.emf/.wmf file or 'stdin'>\n" +
  52. (error == null ? "" : ("Error: "+error+"\n")) +
  53. "Options:\n" +
  54. " -scale <float> scale factor\n" +
  55. " -fixSide <side> specify side (long,short,width,height) to fix - use <scale> as amount of pixels\n" +
  56. " -slide <integer> 1-based index of a slide to render\n" +
  57. " -format <type> png,gif,jpg,svg (,null for testing)\n" +
  58. " -outdir <dir> output directory, defaults to origin of the ppt/pptx file\n" +
  59. " -outfile <file> output filename, defaults to '"+OUTPUT_PAT_REGEX+"'\n" +
  60. " -outpat <pattern> output filename pattern, defaults to '"+OUTPUT_PAT_REGEX+"'\n" +
  61. " patterns: basename, slideno, format, ext\n" +
  62. " -dump <file> dump the annotated records to a file\n" +
  63. " -quiet do not write to console (for normal processing)\n" +
  64. " -ignoreParse ignore parsing error and continue with the records read until the error\n" +
  65. " -extractEmbedded extract embedded parts\n" +
  66. " -inputType <type> default input file type (OLE2,WMF,EMF), default is OLE2 = Powerpoint\n" +
  67. " some files (usually wmf) don't have a header, i.e. an identifiable file magic\n" +
  68. " -textAsShapes text elements are saved as shapes in SVG, necessary for variable spacing\n" +
  69. " often found in math formulas\n" +
  70. " -charset <cs> sets the default charset to be used, defaults to Windows-1252";
  71. System.out.println(msg);
  72. // no System.exit here, as we also run in junit tests!
  73. }
  74. public static void main(String[] args) throws Exception {
  75. PPTX2PNG p2p = new PPTX2PNG();
  76. if (p2p.parseCommandLine(args)) {
  77. p2p.processFile();
  78. }
  79. }
  80. private String slidenumStr = "-1";
  81. private float scale = 1;
  82. private File file = null;
  83. private String format = "png";
  84. private File outdir = null;
  85. private String outfile = null;
  86. private boolean quiet = false;
  87. private String outPattern = OUTPUT_PAT_REGEX;
  88. private File dumpfile = null;
  89. private String fixSide = "scale";
  90. private boolean ignoreParse = false;
  91. private boolean extractEmbedded = false;
  92. private FileMagic defaultFileType = FileMagic.OLE2;
  93. private boolean textAsShapes = false;
  94. private Charset charset = LocaleUtil.CHARSET_1252;
  95. private PPTX2PNG() {
  96. }
  97. private boolean parseCommandLine(String[] args) {
  98. if (args.length == 0) {
  99. usage(null);
  100. return false;
  101. }
  102. for (int i = 0; i < args.length; i++) {
  103. String opt = (i+1 < args.length) ? args[i+1] : null;
  104. switch (args[i].toLowerCase(Locale.ROOT)) {
  105. case "-scale":
  106. if (opt != null) {
  107. scale = Float.parseFloat(opt);
  108. i++;
  109. }
  110. break;
  111. case "-slide":
  112. slidenumStr = opt;
  113. i++;
  114. break;
  115. case "-format":
  116. format = opt;
  117. i++;
  118. break;
  119. case "-outdir":
  120. if (opt != null) {
  121. outdir = new File(opt);
  122. i++;
  123. }
  124. break;
  125. case "-outfile":
  126. outfile = opt;
  127. i++;
  128. break;
  129. case "-outpat":
  130. outPattern = opt;
  131. i++;
  132. break;
  133. case "-quiet":
  134. quiet = true;
  135. break;
  136. case "-dump":
  137. if (opt != null) {
  138. dumpfile = new File(opt);
  139. i++;
  140. } else {
  141. dumpfile = new File("pptx2png.dump");
  142. }
  143. break;
  144. case "-fixside":
  145. if (opt != null) {
  146. fixSide = opt.toLowerCase(Locale.ROOT);
  147. i++;
  148. } else {
  149. fixSide = "long";
  150. }
  151. break;
  152. case "-inputtype":
  153. if (opt != null) {
  154. defaultFileType = FileMagic.valueOf(opt);
  155. i++;
  156. } else {
  157. defaultFileType = FileMagic.OLE2;
  158. }
  159. break;
  160. case "-textasshapes":
  161. textAsShapes = true;
  162. break;
  163. case "-ignoreparse":
  164. ignoreParse = true;
  165. break;
  166. case "-extractembedded":
  167. extractEmbedded = true;
  168. break;
  169. case "-charset":
  170. if (opt != null) {
  171. charset = Charset.forName(opt);
  172. i++;
  173. } else {
  174. charset = LocaleUtil.CHARSET_1252;
  175. }
  176. break;
  177. default:
  178. file = new File(args[i]);
  179. break;
  180. }
  181. }
  182. final boolean isStdin = file != null && "stdin".equalsIgnoreCase(file.getName());
  183. if (!isStdin && (file == null || !file.exists())) {
  184. usage("File not specified or it doesn't exist");
  185. return false;
  186. }
  187. if (format == null || !format.matches("^(png|gif|jpg|null|svg)$")) {
  188. usage("Invalid format given");
  189. return false;
  190. }
  191. if (outdir == null) {
  192. if (isStdin) {
  193. usage("When reading from STDIN, you need to specify an outdir.");
  194. return false;
  195. } else {
  196. outdir = file.getAbsoluteFile().getParentFile();
  197. }
  198. }
  199. if (!outdir.exists()) {
  200. usage("Outdir doesn't exist");
  201. return false;
  202. }
  203. if (!"null".equals(format) && (outdir == null || !outdir.exists() || !outdir.isDirectory())) {
  204. usage("Output directory doesn't exist");
  205. return false;
  206. }
  207. if (scale < 0) {
  208. usage("Invalid scale given");
  209. return false;
  210. }
  211. if (!"long,short,width,height,scale".contains(fixSide)) {
  212. usage("<fixside> must be one of long / short / width / height");
  213. return false;
  214. }
  215. return true;
  216. }
  217. private void processFile() throws IOException {
  218. if (!quiet) {
  219. System.out.println("Processing " + file);
  220. }
  221. try (MFProxy proxy = initProxy(file)) {
  222. final Set<Integer> slidenum = proxy.slideIndexes(slidenumStr);
  223. if (slidenum.isEmpty()) {
  224. usage("slidenum must be either -1 (for all) or within range: [1.." + proxy.getSlideCount() + "] for " + file);
  225. return;
  226. }
  227. final Dimension2D dim = new Dimension2DDouble();
  228. final double lenSide = getDimensions(proxy, dim);
  229. final int width = Math.max((int)Math.rint(dim.getWidth()),1);
  230. final int height = Math.max((int)Math.rint(dim.getHeight()),1);
  231. for (int slideNo : slidenum) {
  232. proxy.setSlideNo(slideNo);
  233. if (!quiet) {
  234. String title = proxy.getTitle();
  235. System.out.println("Rendering slide " + slideNo + (title == null ? "" : ": " + title.trim()));
  236. }
  237. dumpRecords(proxy);
  238. extractEmbedded(proxy, slideNo);
  239. try (OutputFormat outputFormat = ("svg".equals(format)) ? new SVGFormat(textAsShapes) : new BitmapFormat(format)) {
  240. Graphics2D graphics = outputFormat.getGraphics2D(width, height);
  241. // default rendering options
  242. graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
  243. graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
  244. graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
  245. graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
  246. graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
  247. graphics.setRenderingHint(Drawable.DEFAULT_CHARSET, getDefaultCharset());
  248. graphics.scale(scale / lenSide, scale / lenSide);
  249. graphics.setComposite(AlphaComposite.Clear);
  250. graphics.fillRect(0, 0, width, height);
  251. graphics.setComposite(AlphaComposite.SrcOver);
  252. // draw stuff
  253. proxy.draw(graphics);
  254. outputFormat.writeOut(proxy, new File(outdir, calcOutFile(proxy, slideNo)));
  255. }
  256. }
  257. } catch (NoScratchpadException e) {
  258. usage("'"+file.getName()+"': Format not supported - try to include poi-scratchpad.jar into the CLASSPATH.");
  259. return;
  260. }
  261. if (!quiet) {
  262. System.out.println("Done");
  263. }
  264. }
  265. private double getDimensions(MFProxy proxy, Dimension2D dim) {
  266. final Dimension2D pgsize = proxy.getSize();
  267. final double lenSide;
  268. switch (fixSide) {
  269. default:
  270. case "scale":
  271. lenSide = 1;
  272. break;
  273. case "long":
  274. lenSide = Math.max(pgsize.getWidth(), pgsize.getHeight());
  275. break;
  276. case "short":
  277. lenSide = Math.min(pgsize.getWidth(), pgsize.getHeight());
  278. break;
  279. case "width":
  280. lenSide = pgsize.getWidth();
  281. break;
  282. case "height":
  283. lenSide = pgsize.getHeight();
  284. break;
  285. }
  286. dim.setSize(pgsize.getWidth() * scale / lenSide, pgsize.getHeight() * scale / lenSide);
  287. return lenSide;
  288. }
  289. private void dumpRecords(MFProxy proxy) throws IOException {
  290. if (dumpfile == null || "null".equals(dumpfile.getPath())) {
  291. return;
  292. }
  293. GenericRecord gr = proxy.getRoot();
  294. try (GenericRecordJsonWriter fw = new GenericRecordJsonWriter(dumpfile) {
  295. protected boolean printBytes(String name, Object o) {
  296. return false;
  297. }
  298. }) {
  299. if (gr == null) {
  300. fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file.");
  301. } else {
  302. fw.write(gr);
  303. }
  304. }
  305. }
  306. private void extractEmbedded(MFProxy proxy, int slideNo) throws IOException {
  307. if (!extractEmbedded) {
  308. return;
  309. }
  310. for (EmbeddedPart ep : proxy.getEmbeddings(slideNo)) {
  311. String filename = ep.getName();
  312. // do some sanitizing for creative filenames ...
  313. filename = new File(filename == null ? "dummy.dat" : filename).getName();
  314. filename = calcOutFile(proxy, slideNo).replaceFirst("\\.\\w+$", "")+"_"+filename;
  315. try (FileOutputStream fos = new FileOutputStream(new File(outdir, filename))) {
  316. fos.write(ep.getData().get());
  317. }
  318. }
  319. }
  320. private interface ProxyConsumer {
  321. void parse(MFProxy proxy) throws IOException;
  322. }
  323. @SuppressWarnings({"resource", "squid:S2095"})
  324. private MFProxy initProxy(File file) throws IOException {
  325. MFProxy proxy;
  326. final String fileName = file.getName().toLowerCase(Locale.ROOT);
  327. FileMagic fm;
  328. ProxyConsumer con;
  329. if ("stdin".equals(fileName)) {
  330. InputStream bis = FileMagic.prepareToCheckMagic(System.in);
  331. fm = FileMagic.valueOf(bis);
  332. con = (p) -> p.parse(bis);
  333. } else {
  334. fm = FileMagic.valueOf(file);
  335. con = (p) -> p.parse(file);
  336. }
  337. if (fm == FileMagic.UNKNOWN) {
  338. fm = defaultFileType;
  339. }
  340. switch (fm) {
  341. case EMF:
  342. proxy = new EMFHandler();
  343. break;
  344. case WMF:
  345. proxy = new WMFHandler();
  346. break;
  347. default:
  348. proxy = new PPTHandler();
  349. break;
  350. }
  351. proxy.setIgnoreParse(ignoreParse);
  352. proxy.setQuite(quiet);
  353. con.parse(proxy);
  354. proxy.setDefaultCharset(charset);
  355. return proxy;
  356. }
  357. private String calcOutFile(MFProxy proxy, int slideNo) {
  358. if (outfile != null) {
  359. return outfile;
  360. }
  361. String inname = String.format(Locale.ROOT, "%04d|%s|%s", slideNo, format, file.getName());
  362. String outpat = (proxy.getSlideCount() > 1 ? outPattern : outPattern.replaceAll("-?\\$\\{slideno}", ""));
  363. return INPUT_PATTERN.matcher(inname).replaceAll(outpat);
  364. }
  365. private Charset getDefaultCharset() {
  366. return charset;
  367. }
  368. static class NoScratchpadException extends IOException {
  369. NoScratchpadException() {
  370. }
  371. NoScratchpadException(Throwable cause) {
  372. super(cause);
  373. }
  374. }
  375. }