001/******************************************************************************* 002 * This software is provided as a supplement to the authors' textbooks on digital 003 * image processing published by Springer-Verlag in various languages and editions. 004 * Permission to use and distribute this software is granted under the BSD 2-Clause 005 * "Simplified" License (see http://opensource.org/licenses/BSD-2-Clause). 006 * Copyright (c) 2006-2023 Wilhelm Burger, Mark J. Burge. All rights reserved. 007 * Visit https://imagingbook.com for additional details. 008 ******************************************************************************/ 009package imagingbook.pdf; 010 011import com.lowagie.text.BadElementException; 012import com.lowagie.text.Document; 013import com.lowagie.text.DocumentException; 014import com.lowagie.text.Image; 015import com.lowagie.text.Rectangle; 016import com.lowagie.text.pdf.PdfContentByte; 017import com.lowagie.text.pdf.PdfGraphics2D; 018import com.lowagie.text.pdf.PdfWriter; 019import ij.ImagePlus; 020import ij.gui.Overlay; 021import ij.gui.Roi; 022import imagingbook.common.ij.DialogUtils.DialogDigits; 023import imagingbook.common.ij.DialogUtils.DialogHide; 024import imagingbook.common.ij.DialogUtils.DialogLabel; 025import imagingbook.common.util.ParameterBundle; 026import imagingbook.core.Info; 027 028import java.awt.Graphics2D; 029import java.awt.color.ColorSpace; 030import java.awt.color.ICC_Profile; 031import java.io.FileNotFoundException; 032import java.io.FileOutputStream; 033import java.io.IOException; 034import java.nio.file.Path; 035 036/** 037 * Exports an {@link ImagePlus} instance, including its vector overly (if existent) to a PDF file. Configuration is 038 * accomplished by passing a {@link PdfExporter.Parameters} instance to the constructor. If selected, core Type1 fonts 039 * are substituted and embedded in the PDF. 040 * 041 * @author WB 042 * @version 2022/03/15 043 */ 044public class PdfExporter { 045 046 private static double MinStrokeWidth = 0.01; // substitute for zero-width strokes 047 048 private final Parameters params; 049 private final ImagePlus im; 050 051 /** 052 * Parameter bundle for class {@link PdfExporter}. Annotations are used for inserting the complete parameter bundle 053 * in ImageJ's {@code GenericDialog}. 054 */ 055 public static class Parameters implements ParameterBundle<PdfExporter> { 056 057 @DialogLabel("Author:") 058 public String author = System.getProperty("user.name"); 059 @DialogLabel("Title:") 060 public String title = null; 061 062 @DialogLabel("Subject:") 063 public String subject = ""; 064 065 @DialogLabel("Keywords:") 066 public String keywords = ""; 067 068 @DialogLabel("Include raster image:") 069 public boolean includeImage = true; 070 @DialogLabel("Upscale image:") 071 public boolean upscaleImage = true; 072 @DialogLabel("Upscale factor:")@DialogDigits(0) 073 public int upscaleFactor = 1; 074 @DialogLabel("Add sRGB color profile:") 075 public boolean addIccProfile = true; 076 077 @DialogLabel("Include vector overlay:") 078 public boolean includeOverlay = true; 079 @DialogLabel("Min. stroke width:")@DialogDigits(2) 080 public double minStrokeWidth = MinStrokeWidth; 081 @DialogLabel("Embed core fonts:") 082 public boolean embedCoreFonts = true; 083 084 @DialogLabel("Include image props:") 085 public boolean includeImageProps = true; 086 087 @DialogHide 088 public boolean debug = false; 089 090 @Override 091 public boolean validate() { 092 return 093 upscaleFactor >= 1 && 094 minStrokeWidth >= 0; 095 } 096 } 097 098 // ---------------------------------------------------------------------------- 099 100 public PdfExporter(ImagePlus im, Parameters params) { 101 this.im = im; 102 this.params = params; 103 } 104 105 public PdfExporter(ImagePlus im) { 106 this(im, new Parameters()); 107 } 108 109 // ---------------------------------------------------------------------------- 110 111 public String exportTo(Path path) { 112 final int width = im.getWidth(); // original image size 113 final int height = im.getHeight(); 114 Overlay overlay = im.getOverlay(); // original overlay (if any) 115 116 // Step 1: create the PDF document 117 Rectangle pageSize = new Rectangle(width, height); 118 119 try (Document document = new Document(pageSize)) { 120 121 // Step 2: create a PDF writer 122 PdfWriter writer = null; 123 try { 124 writer = PdfWriter.getInstance(document, new FileOutputStream(path.toFile())); 125 } catch (DocumentException | FileNotFoundException e) { 126 if (params.debug) System.out.println("Could no create PdfWriter to path " + path.toString()); 127 return null; 128 } 129 130 // Step 3: open the document and set various properties (TODO) 131 document.open(); 132 document.addTitle(params.title); 133 document.addAuthor(params.author); 134 document.addSubject(params.subject); 135 document.addKeywords(params.keywords); 136 document.addCreationDate(); 137 document.addCreator("ImageJ Plugin (imagingbook)"); 138 document.addProducer(this.getClass().getName() + " " + Info.getVersionInfo()); 139 140 // Step 4: create a template and the associated Graphics2D context 141 PdfContentByte cb = writer.getDirectContent(); 142 143 // Step 5: set sRGB default viewing profile 144 if (params.addIccProfile) { 145 ICC_Profile colorProfile = ICC_Profile.getInstance(ColorSpace.CS_sRGB); 146 try { 147 writer.setOutputIntents("Custom", null, "http://www.color.org", "sRGB IEC61966-2.1", colorProfile); 148 } catch (IOException e) { 149 if (params.debug) System.out.println("Could not add ICC profile!"); 150 } 151 //byte[] iccdata = java.awt.color.ICC_Profile.getInstance(ColorSpace.CS_sRGB).getData(); 152 //com.itextpdf.text.pdf.ICC_Profile icc = com.itextpdf.text.pdf.ICC_Profile.getInstance(iccdata); 153 } 154 155 // Step 6: insert the image 156 if (params.includeImage) { 157 ImagePlus im2 = this.im; 158 int upscale = params.upscaleFactor; 159 if (upscale > 1) { // upscale the image if needed (without interpolation) 160 im2 = im.resize(width * upscale, height * upscale, "none"); 161 } 162 163 try { 164 Image pdfImg = com.lowagie.text.Image.getInstance(im2.getImage(), null); 165 pdfImg.setAbsolutePosition(0, 0); 166 pdfImg.scaleToFit(width, height); // fit to the original size 167 cb.addImage(pdfImg); 168 } catch (BadElementException | IOException e) { 169 if (params.debug) System.out.println("Could not insert image!"); 170 return null; 171 } 172 } 173 174 // Step 7: draw the vector overlay 175 if (overlay != null && params.includeOverlay) { 176 // https://stackoverflow.com/questions/17667615/how-can-itext-embed-font-used-by-jfreechart-for-chart-title-and-labels?rq=1 177 Graphics2D g2 = (params.embedCoreFonts) ? 178 new PdfGraphics2D(cb, width, height, new CoreFontMapper(), false, false, 0) : 179 new PdfGraphics2D(cb, width, height); // no core font embedding 180 181 Roi[] roiArr = overlay.toArray(); 182 for (Roi roi : roiArr) { 183 double sw = roi.getStrokeWidth(); 184 if (sw < params.minStrokeWidth) { // sometimes stroke width is simply not set (= 0) 185 roi.setStrokeWidth(params.minStrokeWidth); // temporarily change stroke width 186 } 187 ImagePlus tmpIm = roi.getImage(); 188 roi.setImage(null); // trick learned from Wayne to ensure magnification is 1 189 roi.drawOverlay(g2); // replacement (recomm. by Wayne) 190 roi.setImage(tmpIm); 191 if (sw < 0.001f) { 192 roi.setStrokeWidth(sw); // restore original stroke width 193 } 194 } 195 g2.dispose(); 196 } 197 198 // Step 8: copy ImagePlus custom properties to PDF 199 if (params.includeImageProps) { 200 String[] props = im.getPropertiesAsArray(); 201 if (props != null) { 202 for (int i = 0; i < props.length; i+=2) { 203 String key = props[i]; 204 String val = props[i + 1]; 205 document.addHeader(key, val); 206 } 207 } 208 } 209 210 // Step 9: close PDF document 211 //document.close(); 212 } // auto-close 213 214 return path.toAbsolutePath().toString(); 215 } 216 217}