/* * This file is part of LaTeXDraw. * Copyright (c) 2005-2017 Arnaud BLOUIN * LaTeXDraw is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License as published by the Free Software * Foundation; either version 2 of the License, or (at your option) any later version. * LaTeXDraw is distributed without any warranty; without even the implied * warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * General Public License for more details. */ package net.sf.latexdraw.view.jfx; import com.sun.pdfview.PDFFile; import com.sun.pdfview.PDFPage; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.OutputStreamWriter; import java.io.PrintWriter; import java.io.RandomAccessFile; import java.io.StringWriter; import java.nio.MappedByteBuffer; import java.nio.channels.FileChannel; import java.util.Optional; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; import javafx.beans.value.ChangeListener; import javafx.embed.swing.SwingFXUtils; import javafx.scene.control.Tooltip; import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.text.Text; import net.sf.latexdraw.actions.ExportFormat; import net.sf.latexdraw.badaboom.BadaboomCollector; import net.sf.latexdraw.models.MathUtils; import net.sf.latexdraw.models.interfaces.shape.Color; import net.sf.latexdraw.models.interfaces.shape.IShape; import net.sf.latexdraw.models.interfaces.shape.IText; import net.sf.latexdraw.util.ImageCropper; import net.sf.latexdraw.util.LFileUtils; import net.sf.latexdraw.util.LSystem; import net.sf.latexdraw.util.OperatingSystem; import net.sf.latexdraw.util.StreamExecReader; import net.sf.latexdraw.util.Triple; import net.sf.latexdraw.util.Tuple; import net.sf.latexdraw.view.latex.DviPsColors; import net.sf.latexdraw.view.latex.LaTeXGenerator; import net.sf.latexdraw.view.pst.PSTricksConstants; /** * The JFX shape view for text shapes. * @author Arnaud Blouin */ public class ViewText extends ViewPositionShape<IText> { private static final ExecutorService COMPILATION_POOL = Executors.newFixedThreadPool(5); private static final double SCALE_COMPILE = 2d; private final Text text; private final ImageView compiledText; private final Tooltip compileTooltip; private final ChangeListener<String> textUpdate; private Future<?> currentCompilation; /** * Creates the view. * @param sh The model. */ ViewText(final IText sh) { super(sh); text = new Text(); compiledText = new ImageView(); compileTooltip = new Tooltip(null); compiledText.setScaleX(1d / SCALE_COMPILE); compiledText.setScaleY(compiledText.getScaleX()); compiledText.setVisible(false); compiledText.setSmooth(true); compiledText.setCache(true); compiledText.imageProperty().addListener((observable, oldValue, newValue) -> { if(newValue != null) { compiledText.setX(-newValue.getWidth() / 4d); compiledText.setY(-newValue.getHeight() * 0.75); } }); textUpdate = (observable, oldValue, newValue) -> update(); model.textProperty().addListener(textUpdate); getChildren().add(text); getChildren().add(compiledText); update(); } private void update() { text.setText(model.getText()); currentCompilation = COMPILATION_POOL.submit(() -> updateImageText(createImage())); } /** * @return The current text compilation. May be null. */ public Future<?> getCurrentCompilation() { return currentCompilation; } private void updateImageText(final Triple<Image, String, String> values) { deleteImage(); if(currentCompilation != null && currentCompilation.isDone()) { currentCompilation = null; } compiledText.setUserData(new Tuple<>(values.b, values.c)); // A text will be used to render the text shape. if(values.a == null) { compileTooltip.setText(LSystem.INSTANCE.getLatexErrorMessageFromLog(values.c)); Tooltip.install(text, compileTooltip); compiledText.setVisible(false); compiledText.setImage(null); text.setVisible(true); }else { // An image will be used to render the text shape. compileTooltip.setText(null); Tooltip.uninstall(text, compileTooltip); compiledText.setVisible(true); text.setVisible(false); compiledText.setImage(values.a); getCanvasParent().ifPresent(canvas -> canvas.update()); } } public Optional<Tuple<String, String>> getCompilationData() { if(compiledText.getUserData() instanceof Tuple) { return Optional.of((Tuple<String, String>) compiledText.getUserData()); } return Optional.empty(); } /** * Deletes the image written on the disk. */ private void deleteImage() { getCompilationData().ifPresent(data -> new File(data.a).delete()); } private String getLaTeXDocument() { final String code = model.getText(); final StringBuilder doc = new StringBuilder(); final Color textColour = model.getLineColour(); boolean coloured = false; // We must scale the text to fit its latex size: latexdrawDPI/latexDPI is the ratio to scale the created png picture. final double scale = IShape.PPC * PSTricksConstants.INCH_VAL_CM / PSTricksConstants.INCH_VAL_PT * SCALE_COMPILE; doc.append("\\documentclass{standalone}\n\\usepackage[usenames,dvipsnames]{pstricks}"); //$NON-NLS-1$ doc.append(LaTeXGenerator.getPackages()).append('\n'); doc.append("\\begin{document}\n\\psscalebox{"); //$NON-NLS-1$ doc.append((float) MathUtils.INST.getCutNumber(scale)).append(' '); doc.append((float) MathUtils.INST.getCutNumber(scale)).append('}').append('{'); if(!textColour.equals(PSTricksConstants.DEFAULT_LINE_COLOR)) { final String name = DviPsColors.INSTANCE.getColourName(textColour).orElse(DviPsColors.INSTANCE.addUserColour(textColour).orElse("")); coloured = true; doc.append(DviPsColors.INSTANCE.getUsercolourCode(name)).append("\\textcolor{").append(name).append('}').append('{'); //$NON-NLS-1$ } doc.append(code); if(coloured) { doc.append('}'); } doc.append("}\n\\end{document}");//$NON-NLS-1$ return doc.toString(); } /** * Executes a given command and returns the log. * @param cmd The command to execute. * @return True if the command exits normally plus the log. */ private Tuple<Boolean, String> execute(final String[] cmd) { String log = ""; try { final Process process = Runtime.getRuntime().exec(cmd); final StreamExecReader errReader = new StreamExecReader(process.getErrorStream()); final StreamExecReader outReader = new StreamExecReader(process.getInputStream()); errReader.start(); outReader.start(); if(process.waitFor() == 0) { return new Tuple<>(true, log); } log = outReader.getLog() + LSystem.EOL + errReader.getLog(); }catch(final Throwable ex) { log += ex.getMessage(); } return new Tuple<>(false, log); } /** * @return The LaTeX compiled picture of the text with its file path and its log. */ private Triple<Image, String, String> createImage() { BufferedImage bi = null; String log = ""; //$NON-NLS-1$ final File tmpDir = LFileUtils.INSTANCE.createTempDir(); final String doc = getLaTeXDocument(); final String pathPic = tmpDir.getAbsolutePath() + LSystem.FILE_SEP + "latexdrawTmpPic" + System.currentTimeMillis(); //$NON-NLS-1$ final String pathTex = pathPic + ExportFormat.TEX.getFileExtension(); final OperatingSystem os = LSystem.INSTANCE.getSystem(); try { final FileOutputStream fos = new FileOutputStream(pathTex); final OutputStreamWriter osw = new OutputStreamWriter(fos); osw.append(doc); osw.flush(); osw.close(); fos.flush(); fos.close(); Tuple<Boolean, String> res = execute(new String[]{os.getLatexBinPath(), "--halt-on-error", "--interaction=nonstopmode", //$NON-NLS-1$ //$NON-NLS-2$ "--output-directory=" + tmpDir.getAbsolutePath(), LFileUtils.INSTANCE.normalizeForLaTeX(pathTex)}); //$NON-NLS-1$ boolean ok = res.a; log = res.b; new File(pathTex).delete(); new File(pathPic + ".aux").delete(); //$NON-NLS-1$ new File(pathPic + ".log").delete(); //$NON-NLS-1$ final String psExt = ".eps"; //$NON-NLS-1$ if(ok) { res = execute(new String[]{os.getDvipsBinPath(), pathPic + ".dvi", "-o", pathPic + psExt}); //$NON-NLS-1$ //$NON-NLS-2$ ok = res.a; log = log + res.b; new File(pathPic + ".dvi").delete(); //$NON-NLS-1$ } if(ok) { res = execute(new String[]{os.getPs2pdfBinPath(), pathPic + psExt, pathPic + ExportFormat.PDF.getFileExtension()}); //$NON-NLS-1$ new File(pathPic + psExt).delete(); //$NON-NLS-1$ ok = res.a; log = log + res.b; } if(ok) try { final File file = new File(pathPic + ExportFormat.PDF.getFileExtension()); final RandomAccessFile raf = new RandomAccessFile(file, "r"); final FileChannel fc = raf.getChannel(); final MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_ONLY, 0, fc.size()); final PDFFile pdfFile = new PDFFile(mbb); mbb.clear(); fc.close(); raf.close(); if(pdfFile.getNumPages() == 1) { final PDFPage page = pdfFile.getPage(0); final Rectangle2D bound = page.getBBox(); final java.awt.Image img = page.getImage((int) bound.getWidth(), (int) bound.getHeight(), bound, null, false, true); if(img instanceof BufferedImage) { bi = ImageCropper.INSTANCE.cropImage((BufferedImage) img); } if(img != null) img.flush(); }else { BadaboomCollector.INSTANCE.add(new IllegalArgumentException("Not a single page: " + pdfFile.getNumPages())); } file.delete(); }catch(final Throwable ex) { BadaboomCollector.INSTANCE.add(ex); } }catch(final Throwable ex) { try(final StringWriter sw = new StringWriter(); final PrintWriter pw = new PrintWriter(sw)) { ex.printStackTrace(pw); new File(pathPic + ExportFormat.TEX.getFileExtension()).delete(); new File(pathPic + ExportFormat.PDF.getFileExtension()).delete(); new File(pathPic + ".ps").delete(); //$NON-NLS-1$ new File(pathPic + ".dvi").delete(); //$NON-NLS-1$ new File(pathPic + ".aux").delete(); //$NON-NLS-1$ new File(pathPic + ".log").delete(); //$NON-NLS-1$ BadaboomCollector.INSTANCE.add(new FileNotFoundException("Log:\n" + log + "\nException:\n" + sw)); }catch(final Throwable ex2) { BadaboomCollector.INSTANCE.add(ex2); } } Image fxImage; if(bi == null) { fxImage = null; }else { fxImage = SwingFXUtils.toFXImage(bi, null); bi.flush(); } return new Triple<>(fxImage, pathPic, log); } @Override public void flush() { model.textProperty().removeListener(textUpdate); deleteImage(); super.flush(); } }