123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427 |
- /*
- * ====================================================================
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements. See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- * ====================================================================
- */
-
- package org.apache.poi.xslf.util;
-
- import java.awt.AlphaComposite;
- import java.awt.Graphics2D;
- import java.awt.RenderingHints;
- import java.awt.geom.Dimension2D;
- import java.io.File;
- import java.io.FileOutputStream;
- import java.io.IOException;
- import java.io.InputStream;
- import java.nio.charset.Charset;
- import java.util.Locale;
- import java.util.Set;
- import java.util.regex.Pattern;
-
- import org.apache.poi.common.usermodel.GenericRecord;
- import org.apache.poi.poifs.filesystem.FileMagic;
- import org.apache.poi.sl.draw.Drawable;
- import org.apache.poi.sl.draw.EmbeddedExtractor.EmbeddedPart;
- import org.apache.poi.util.Dimension2DDouble;
- import org.apache.poi.util.GenericRecordJsonWriter;
- import org.apache.poi.util.LocaleUtil;
- import org.apache.poi.xslf.util.OutputFormat.BitmapFormat;
- import org.apache.poi.xslf.util.OutputFormat.SVGFormat;
-
- /**
- * An utility to convert slides of a .pptx slide show to a PNG image
- */
- public final class PPTX2PNG {
-
- private static final String INPUT_PAT_REGEX =
- "(?<slideno>[^|]+)\\|(?<format>[^|]+)\\|(?<basename>.+)\\.(?<ext>[^.]++)";
-
- private static final Pattern INPUT_PATTERN = Pattern.compile(INPUT_PAT_REGEX);
-
- private static final String OUTPUT_PAT_REGEX = "${basename}-${slideno}.${format}";
-
- private static void usage(String error){
- String msg =
- "Usage: PPTX2PNG [options] <.ppt/.pptx/.emf/.wmf file or 'stdin'>\n" +
- (error == null ? "" : ("Error: "+error+"\n")) +
- "Options:\n" +
- " -scale <float> scale factor\n" +
- " -fixSide <side> specify side (long,short,width,height) to fix - use <scale> as amount of pixels\n" +
- " -slide <integer> 1-based index of a slide to render\n" +
- " -format <type> png,gif,jpg,svg (,null for testing)\n" +
- " -outdir <dir> output directory, defaults to origin of the ppt/pptx file\n" +
- " -outfile <file> output filename, defaults to '"+OUTPUT_PAT_REGEX+"'\n" +
- " -outpat <pattern> output filename pattern, defaults to '"+OUTPUT_PAT_REGEX+"'\n" +
- " patterns: basename, slideno, format, ext\n" +
- " -dump <file> dump the annotated records to a file\n" +
- " -quiet do not write to console (for normal processing)\n" +
- " -ignoreParse ignore parsing error and continue with the records read until the error\n" +
- " -extractEmbedded extract embedded parts\n" +
- " -inputType <type> default input file type (OLE2,WMF,EMF), default is OLE2 = Powerpoint\n" +
- " some files (usually wmf) don't have a header, i.e. an identifiable file magic\n" +
- " -textAsShapes text elements are saved as shapes in SVG, necessary for variable spacing\n" +
- " often found in math formulas\n" +
- " -charset <cs> sets the default charset to be used, defaults to Windows-1252";
-
- System.out.println(msg);
- // no System.exit here, as we also run in junit tests!
- }
-
- public static void main(String[] args) throws Exception {
- PPTX2PNG p2p = new PPTX2PNG();
-
- if (p2p.parseCommandLine(args)) {
- p2p.processFile();
- }
- }
-
- private String slidenumStr = "-1";
- private float scale = 1;
- private File file = null;
- private String format = "png";
- private File outdir = null;
- private String outfile = null;
- private boolean quiet = false;
- private String outPattern = OUTPUT_PAT_REGEX;
- private File dumpfile = null;
- private String fixSide = "scale";
- private boolean ignoreParse = false;
- private boolean extractEmbedded = false;
- private FileMagic defaultFileType = FileMagic.OLE2;
- private boolean textAsShapes = false;
- private Charset charset = LocaleUtil.CHARSET_1252;
-
- private PPTX2PNG() {
- }
-
- private boolean parseCommandLine(String[] args) {
- if (args.length == 0) {
- usage(null);
- return false;
- }
-
- for (int i = 0; i < args.length; i++) {
- String opt = (i+1 < args.length) ? args[i+1] : null;
- switch (args[i].toLowerCase(Locale.ROOT)) {
- case "-scale":
- if (opt != null) {
- scale = Float.parseFloat(opt);
- i++;
- }
- break;
- case "-slide":
- slidenumStr = opt;
- i++;
- break;
- case "-format":
- format = opt;
- i++;
- break;
- case "-outdir":
- if (opt != null) {
- outdir = new File(opt);
- i++;
- }
- break;
- case "-outfile":
- outfile = opt;
- i++;
- break;
- case "-outpat":
- outPattern = opt;
- i++;
- break;
- case "-quiet":
- quiet = true;
- break;
- case "-dump":
- if (opt != null) {
- dumpfile = new File(opt);
- i++;
- } else {
- dumpfile = new File("pptx2png.dump");
- }
- break;
- case "-fixside":
- if (opt != null) {
- fixSide = opt.toLowerCase(Locale.ROOT);
- i++;
- } else {
- fixSide = "long";
- }
- break;
- case "-inputtype":
- if (opt != null) {
- defaultFileType = FileMagic.valueOf(opt);
- i++;
- } else {
- defaultFileType = FileMagic.OLE2;
- }
- break;
- case "-textasshapes":
- textAsShapes = true;
- break;
- case "-ignoreparse":
- ignoreParse = true;
- break;
- case "-extractembedded":
- extractEmbedded = true;
- break;
- case "-charset":
- if (opt != null) {
- charset = Charset.forName(opt);
- i++;
- } else {
- charset = LocaleUtil.CHARSET_1252;
- }
- break;
-
- default:
- file = new File(args[i]);
- break;
- }
- }
-
- final boolean isStdin = file != null && "stdin".equalsIgnoreCase(file.getName());
-
- if (!isStdin && (file == null || !file.exists())) {
- usage("File not specified or it doesn't exist");
- return false;
- }
-
- if (format == null || !format.matches("^(png|gif|jpg|null|svg)$")) {
- usage("Invalid format given");
- return false;
- }
-
- if (outdir == null) {
- if (isStdin) {
- usage("When reading from STDIN, you need to specify an outdir.");
- return false;
- } else {
- outdir = file.getAbsoluteFile().getParentFile();
- }
- }
- if (!outdir.exists()) {
- usage("Outdir doesn't exist");
- return false;
- }
-
- if (!"null".equals(format) && (outdir == null || !outdir.exists() || !outdir.isDirectory())) {
- usage("Output directory doesn't exist");
- return false;
- }
-
- if (scale < 0) {
- usage("Invalid scale given");
- return false;
- }
-
- if (!"long,short,width,height,scale".contains(fixSide)) {
- usage("<fixside> must be one of long / short / width / height");
- return false;
- }
-
- return true;
- }
-
- private void processFile() throws IOException {
- if (!quiet) {
- System.out.println("Processing " + file);
- }
-
-
-
- try (MFProxy proxy = initProxy(file)) {
- final Set<Integer> slidenum = proxy.slideIndexes(slidenumStr);
- if (slidenum.isEmpty()) {
- usage("slidenum must be either -1 (for all) or within range: [1.." + proxy.getSlideCount() + "] for " + file);
- return;
- }
-
- final Dimension2D dim = new Dimension2DDouble();
- final double lenSide = getDimensions(proxy, dim);
- final int width = Math.max((int)Math.rint(dim.getWidth()),1);
- final int height = Math.max((int)Math.rint(dim.getHeight()),1);
-
- for (int slideNo : slidenum) {
- proxy.setSlideNo(slideNo);
- if (!quiet) {
- String title = proxy.getTitle();
- System.out.println("Rendering slide " + slideNo + (title == null ? "" : ": " + title.trim()));
- }
-
- dumpRecords(proxy);
-
- extractEmbedded(proxy, slideNo);
-
- try (OutputFormat outputFormat = ("svg".equals(format)) ? new SVGFormat(textAsShapes) : new BitmapFormat(format)) {
- Graphics2D graphics = outputFormat.getGraphics2D(width, height);
-
- // default rendering options
- graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
- graphics.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
- graphics.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_SPEED);
- graphics.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
- graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
- graphics.setRenderingHint(Drawable.DEFAULT_CHARSET, getDefaultCharset());
-
- graphics.scale(scale / lenSide, scale / lenSide);
-
- graphics.setComposite(AlphaComposite.Clear);
- graphics.fillRect(0, 0, width, height);
- graphics.setComposite(AlphaComposite.SrcOver);
-
- // draw stuff
- proxy.draw(graphics);
-
- outputFormat.writeOut(proxy, new File(outdir, calcOutFile(proxy, slideNo)));
- }
- }
- } catch (NoScratchpadException e) {
- usage("'"+file.getName()+"': Format not supported - try to include poi-scratchpad.jar into the CLASSPATH.");
- return;
- }
-
- if (!quiet) {
- System.out.println("Done");
- }
- }
-
- private double getDimensions(MFProxy proxy, Dimension2D dim) {
- final Dimension2D pgsize = proxy.getSize();
-
- final double lenSide;
- switch (fixSide) {
- default:
- case "scale":
- lenSide = 1;
- break;
- case "long":
- lenSide = Math.max(pgsize.getWidth(), pgsize.getHeight());
- break;
- case "short":
- lenSide = Math.min(pgsize.getWidth(), pgsize.getHeight());
- break;
- case "width":
- lenSide = pgsize.getWidth();
- break;
- case "height":
- lenSide = pgsize.getHeight();
- break;
- }
-
- dim.setSize(pgsize.getWidth() * scale / lenSide, pgsize.getHeight() * scale / lenSide);
- return lenSide;
- }
-
- private void dumpRecords(MFProxy proxy) throws IOException {
- if (dumpfile == null || "null".equals(dumpfile.getPath())) {
- return;
- }
- GenericRecord gr = proxy.getRoot();
- try (GenericRecordJsonWriter fw = new GenericRecordJsonWriter(dumpfile) {
- protected boolean printBytes(String name, Object o) {
- return false;
- }
- }) {
- if (gr == null) {
- fw.writeError(file.getName()+" doesn't support GenericRecord interface and can't be dumped to a file.");
- } else {
- fw.write(gr);
- }
- }
- }
-
- private void extractEmbedded(MFProxy proxy, int slideNo) throws IOException {
- if (!extractEmbedded) {
- return;
- }
- for (EmbeddedPart ep : proxy.getEmbeddings(slideNo)) {
- String filename = ep.getName();
- // do some sanitizing for creative filenames ...
- filename = new File(filename == null ? "dummy.dat" : filename).getName();
- filename = calcOutFile(proxy, slideNo).replaceFirst("\\.\\w+$", "")+"_"+filename;
- try (FileOutputStream fos = new FileOutputStream(new File(outdir, filename))) {
- fos.write(ep.getData().get());
- }
- }
- }
-
- private interface ProxyConsumer {
- void parse(MFProxy proxy) throws IOException;
- }
-
- @SuppressWarnings({"resource", "squid:S2095"})
- private MFProxy initProxy(File file) throws IOException {
- MFProxy proxy;
- final String fileName = file.getName().toLowerCase(Locale.ROOT);
- FileMagic fm;
- ProxyConsumer con;
- if ("stdin".equals(fileName)) {
- InputStream bis = FileMagic.prepareToCheckMagic(System.in);
- fm = FileMagic.valueOf(bis);
- con = (p) -> p.parse(bis);
- } else {
- fm = FileMagic.valueOf(file);
- con = (p) -> p.parse(file);
- }
-
- if (fm == FileMagic.UNKNOWN) {
- fm = defaultFileType;
- }
- switch (fm) {
- case EMF:
- proxy = new EMFHandler();
- break;
- case WMF:
- proxy = new WMFHandler();
- break;
- default:
- proxy = new PPTHandler();
- break;
- }
- proxy.setIgnoreParse(ignoreParse);
- proxy.setQuite(quiet);
- con.parse(proxy);
- proxy.setDefaultCharset(charset);
-
- return proxy;
- }
-
- private String calcOutFile(MFProxy proxy, int slideNo) {
- if (outfile != null) {
- return outfile;
- }
- String inname = String.format(Locale.ROOT, "%04d|%s|%s", slideNo, format, file.getName());
- String outpat = (proxy.getSlideCount() > 1 ? outPattern : outPattern.replaceAll("-?\\$\\{slideno}", ""));
- return INPUT_PATTERN.matcher(inname).replaceAll(outpat);
- }
-
- private Charset getDefaultCharset() {
- return charset;
- }
-
- static class NoScratchpadException extends IOException {
- NoScratchpadException() {
- }
-
- NoScratchpadException(Throwable cause) {
- super(cause);
- }
- }
- }
|