/*
* uDig - User Friendly Desktop Internet GIS client
* http://udig.refractions.net
* (C) 2012, Refractions Research Inc.
*
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* (http://www.eclipse.org/legal/epl-v10.html), and the Refractions BSD
* License v1.0 (http://udig.refractions.net/files/bsd3-v10.html).
*/
package org.locationtech.udig.printing.ui.pdf;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.print.PageFormat;
import java.awt.print.PrinterException;
import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.net.URL;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import javax.imageio.ImageIO;
import org.locationtech.udig.catalog.IGeoResource;
import org.locationtech.udig.core.internal.Icons;
import org.locationtech.udig.printing.model.Box;
import org.locationtech.udig.printing.model.ModelFactory;
import org.locationtech.udig.printing.model.Page;
import org.locationtech.udig.printing.ui.Template;
import org.locationtech.udig.printing.ui.TemplateFactory;
import org.locationtech.udig.printing.ui.internal.Messages;
import org.locationtech.udig.printing.ui.internal.PrintingEngine;
import org.locationtech.udig.printing.ui.internal.PrintingPlugin;
import org.locationtech.udig.project.IBlackboard;
import org.locationtech.udig.project.internal.Layer;
import org.locationtech.udig.project.internal.Map;
import org.locationtech.udig.project.internal.command.navigation.SetViewportBBoxCommand;
import org.locationtech.udig.project.ui.ApplicationGIS;
import org.locationtech.udig.project.ui.BoundsStrategy;
import org.locationtech.udig.project.ui.SelectionStyle;
import org.locationtech.udig.project.ui.ApplicationGIS.DrawMapParameter;
import org.locationtech.udig.project.ui.internal.MapEditorInput;
import org.eclipse.draw2d.geometry.Dimension;
import org.eclipse.jface.resource.ImageDescriptor;
import org.eclipse.jface.resource.ImageRegistry;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.wizard.Wizard;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IExportWizard;
import org.eclipse.ui.IWorkbench;
import org.eclipse.ui.PlatformUI;
import org.opengis.coverage.grid.GridCoverageReader;
import com.lowagie.text.Document;
import com.lowagie.text.DocumentException;
import com.lowagie.text.Image;
import com.lowagie.text.PageSize;
import com.lowagie.text.Rectangle;
import com.lowagie.text.pdf.PdfContentByte;
import com.lowagie.text.pdf.PdfWriter;
/**
* A user interface for choosing an export template and other export options.
* The wizard is configurable by placing an ExportPDFWizardConfigBean on the
* map blackboard (see ExportPDFWizardConfigBean for the blackboard key).
* <p>
*
* </p>
* @author brocka
* @since 1.1.0
*/
public class ExportPDFWizard extends Wizard implements IExportWizard {
private Map map;
private ExportPDFWizardPage1 page1;
public ExportPDFWizard(){
PrintingPlugin plugin = PrintingPlugin.getDefault();
java.util.Map<String, TemplateFactory> templateFactories = plugin.getTemplateFactories();
setWindowTitle(Messages.ExportPDFWizard_Title);
String key = Icons.WIZBAN +"exportpdf_wiz.gif"; //$NON-NLS-1$
ImageRegistry imageRegistry = plugin.getImageRegistry();
ImageDescriptor image = imageRegistry.getDescriptor( key );
if( image == null ){
URL banURL = plugin.getBundle().getResource( "icons/" + key ); //$NON-NLS-1$
image = ImageDescriptor.createFromURL( banURL );
imageRegistry.put( key, image );
}
setDefaultPageImageDescriptor( image );
//get copy of map
IEditorInput input = PlatformUI.getWorkbench().getActiveWorkbenchWindow().getActivePage()
.getActiveEditor().getEditorInput();
map = (Map) ((MapEditorInput) input).getProjectElement();
//get configuration for this wizard
IBlackboard mapBlackboard = map.getBlackboard();
ExportPDFWizardConfigBean config =
(ExportPDFWizardConfigBean)mapBlackboard.get(ExportPDFWizardConfigBean.BLACKBOARD_KEY);
page1 = new ExportPDFWizardPage1(templateFactories, config);
addPage(page1);
}
@Override
public boolean canFinish() {
return page1.isPageComplete();
}
@Override
public boolean performFinish() {
//create the document
Rectangle suggestedPageSize = getITextPageSize(page1.getPageSize());
Rectangle pageSize = rotatePageIfNecessary(suggestedPageSize); //rotate if we need landscape
Document document = new Document(pageSize);
try {
//Basic setup of the Document, and get instance of the iText Graphics2D
// to pass along to uDig's standard "printing" code.
String outputFile = page1.getDestinationDir()+
System.getProperty("file.separator")+ //$NON-NLS-1$
page1.getOutputFile();
PdfWriter writer = PdfWriter.getInstance(document, new FileOutputStream(outputFile));
document.open();
Graphics2D graphics = null;
Template template = getTemplate();
int i = 0;
int numPages = 1;
do {
//sets the active page
template.setActivePage(i);
PdfContentByte cb = writer.getDirectContent();
Page page = makePage(pageSize, document, template);
graphics = cb.createGraphics(pageSize.getWidth(), pageSize.getHeight());
//instantiate a PrinterEngine (pass in the Page instance)
PrintingEngine engine = new PrintingEngine(page);
//make page format
PageFormat pageFormat = new PageFormat();
pageFormat.setOrientation(PageFormat.PORTRAIT);
java.awt.print.Paper awtPaper = new java.awt.print.Paper();
awtPaper.setSize(pageSize.getWidth() * 3, pageSize.getHeight() *3);
awtPaper.setImageableArea(0, 0, pageSize.getWidth(), pageSize.getHeight());
pageFormat.setPaper(awtPaper);
//run PrinterEngine's print function
engine.print(graphics, pageFormat, 0);
graphics.dispose();
document.newPage();
if (i == 0) {
numPages = template.getNumPages();
}
i++;
} while (i < numPages);
//cleanup
document.close();
writer.close();
}
catch (DocumentException e) {
e.printStackTrace();
return false;
}
catch (IOException e) {
e.printStackTrace();
return false;
} catch (PrinterException e) {
e.printStackTrace();
}
return true;
}
/**
* converts a page size "name" (such as "A3" or "A4" into a
* rectangle object that iText will understand.
*/
private Rectangle getITextPageSize(String pageSizeName) {
if (pageSizeName.equals("A3")) //$NON-NLS-1$
return PageSize.A3;
if (pageSizeName.equals("A4")) //$NON-NLS-1$
return PageSize.A4;
throw new IllegalArgumentException(pageSizeName + " is not a supported page size"); //$NON-NLS-1$
}
protected Rectangle rotatePageIfNecessary(Rectangle suggestedPageSize) {
//rotate the page if dimensions are given as portrait, but template prefers landscape
if (suggestedPageSize.getHeight() > suggestedPageSize.getWidth() && page1.isLandscape()) {
float temp = suggestedPageSize.getWidth();
float newWidth = suggestedPageSize.getHeight();
float newHeight = temp;
return new Rectangle(suggestedPageSize.getHeight(), suggestedPageSize.getWidth());
}
return suggestedPageSize;
}
/**
* Creates a page based on the template selected in the wizard
* **Note: this function may swap the width and height if the
* template prefers different page orientation.
*
* @return a page
*/
protected Page makePage(Rectangle pageSize, Document doc, Template template) {
Map mapOnlyRasterLayers = null;
Map mapNoRasterLayers = null;
// **Note: the iText API doesn't render rasters at a high enough resolution if
//they are written to the PDF via graphics2d. To work around this problem, I
//create two copies of the map: one with only the raster layers, and one with
//everything else.
//The "everything else" map gets drawn by a graphics2d. The other layer must be
//rasterized and inserted into the PDF via iText's API.
//make one copy of the map with no raster layers
mapNoRasterLayers = (Map) ApplicationGIS.copyMap(map);
List<Layer> layersNoRasters = mapNoRasterLayers.getLayersInternal();
List<Layer> toRemove = new ArrayList<Layer>();
for (Layer layer : layersNoRasters ) {
for (IGeoResource resource : layer.getGeoResources()) {
if (resource.canResolve(GridCoverageReader.class)) {
toRemove.add(layer);
}
}
}
layersNoRasters.removeAll(toRemove);
//adjust scale
double currentViewportScaleDenom = map.getViewportModel().getScaleDenominator();
if (currentViewportScaleDenom == -1)
throw new IllegalStateException("no scale denominator is available from the viewport model"); //$NON-NLS-1$
if (page1.getScaleOption() == PrintWizardPage1.CUSTOM_MAP_SCALE) {
float customScale = page1.getCustomScale();
template.setMapScaleHint(customScale);
}
else if (page1.getScaleOption() == PrintWizardPage1.CURRENT_MAP_SCALE) {
template.setMapScaleHint(currentViewportScaleDenom);
}
else if (page1.getScaleOption() == PrintWizardPage1.ZOOM_TO_SELECTION) {
template.setZoomToSelectionHint(true);
template.setMapScaleHint(currentViewportScaleDenom);
}
//3. make the page itself
Page page = ModelFactory.eINSTANCE.createPage();
page.setSize(new Dimension((int)pageSize.getWidth(), (int)pageSize.getHeight()));
//page name stuff not required, because this page will just get discarded
MessageFormat formatter = new MessageFormat(Messages.CreatePageAction_newPageName, Locale.getDefault());
if (page.getName() == null || page.getName().length() == 0) {
page.setName(formatter.format(new Object[] { mapNoRasterLayers.getName() }));
}
template.init(page, mapNoRasterLayers);
if (page1.getRasterEnabled()) {
//make another copy with only raster layers
mapOnlyRasterLayers = (Map) ApplicationGIS.copyMap(map);
List<Layer> layersOnlyRasters = mapOnlyRasterLayers.getLayersInternal();
List<Layer> toRemove2 = new ArrayList<Layer>();
for (Layer layer : layersOnlyRasters ) {
for (IGeoResource resource : layer.getGeoResources()) {
if (!resource.canResolve(GridCoverageReader.class)) {
toRemove2.add(layer);
}
}
}
layersOnlyRasters.removeAll(toRemove2);
//set bounds to match the other map
SetViewportBBoxCommand cmdBbox = new SetViewportBBoxCommand(mapNoRasterLayers.getViewportModel().getBounds());
mapOnlyRasterLayers.sendCommandSync(cmdBbox);
if (layersNoRasters.size() > 0) {
writeRasterLayersOnlyToDocument(mapOnlyRasterLayers, template.getMapBounds(), doc, page.getSize(), /*currentViewportScaleDenom*/mapNoRasterLayers.getViewportModel().getScaleDenominator());
}
}
//copy the boxes from the template into the page
Iterator<Box> iter = template.iterator();
while (iter.hasNext()) {
page.getBoxes().add(iter.next());
}
return page;
//TODO Throw some sort of exception if the page can't be created
}
/**
* Gets the template selected in the wizard.
*
* @return a template
*/
private Template getTemplate() {
TemplateFactory templateFactory = page1.getTemplateFactory();
Template template = templateFactory.createTemplate();
return template;
}
public void init( IWorkbench workbench, IStructuredSelection selection ) {
}
/**
* double scaleDenom = page1.isCustomScale() ? page1.getCustomScale() : map.getViewportModel().getScaleDenominator();
*
* @param mapWithRasterLayersOnly a map with only raster layers
* @param mapBoundsInTemplate a rectangle indicating the coordinates of the top left, width and height
* (where the coordinate system has (0,0) in the top left.
* @param doc the PDF document object
*/
private void writeRasterLayersOnlyToDocument(Map mapWithRasterLayersOnly,
org.eclipse.swt.graphics.Rectangle mapBoundsInTemplate,
Document doc,
Dimension pageSize,
double currentViewportScaleDenom) {
//set dimensions of the raster image to be the same ratio as
//the required map bounds within the page, but scaled up so
//we can achieve a higher DPI when we later insert the image
//into the page (90 refers to the default DPI)
int w = mapBoundsInTemplate.width * page1.getDpi()/90;
int h = mapBoundsInTemplate.height * page1.getDpi()/90;
BufferedImage imageOfRastersOnly = new BufferedImage(w,
h,
BufferedImage.TYPE_INT_RGB);
Graphics2D g = imageOfRastersOnly.createGraphics();
//define a DrawMapParameter object with a custom BoundsStrategy if a custom scale is set
DrawMapParameter drawMapParameter = null;
double scaleDenom = (page1.getScaleOption() == ExportPDFWizardPage1.CUSTOM_MAP_SCALE) ? page1.getCustomScale() : currentViewportScaleDenom;
drawMapParameter =
new DrawMapParameter(g,
new java.awt.Dimension(w, h),
mapWithRasterLayersOnly,
new BoundsStrategy(scaleDenom),
page1.getDpi(),
SelectionStyle.EXCLUSIVE_ALL,
null);
try {
//draw the map (at a high resolution as specified above)
ApplicationGIS.drawMap(drawMapParameter);
Image img = Image.getInstance(bufferedImage2ByteArray(imageOfRastersOnly));
//scale the image down to fit into the page
img.scaleAbsolute(mapBoundsInTemplate.width, mapBoundsInTemplate.height);
//set the location of the image
int left = mapBoundsInTemplate.x;
int bottom = pageSize.height - mapBoundsInTemplate.height - mapBoundsInTemplate.y;
img.setAbsolutePosition(left, bottom); //(0,0) is bottom left in the PDF coordinate system
doc.add(img);
addWhiteMapBorder(img, doc);
}
catch (Exception e) {
//TODO: fail gracefully.
}
}
/**
* This function is used to draw very thin white borders over the outer
* edge of the raster map. It's necessary because the map edge "bleeds" into
* the adjacent pixels, and we need to cover that.
*
* I think this quirky behaviour is possibly an iText bug
*/
private void addWhiteMapBorder(Image img,
Document doc) {
try {
Color color = Color.white;
int borderWidth = 1;
BufferedImage bufferedTop = new BufferedImage((int)img.getScaledWidth(),
borderWidth,
BufferedImage.TYPE_INT_RGB);
Graphics2D g1 = bufferedTop.createGraphics();
g1.setBackground(color);
g1.clearRect(0, 0, bufferedTop.getWidth(),bufferedTop.getHeight());
Image top = Image.getInstance(bufferedImage2ByteArray(bufferedTop));
top.setAbsolutePosition(img.getAbsoluteX(), img.getAbsoluteY()+img.getScaledHeight()-bufferedTop.getHeight()/2);
BufferedImage bufferedBottom = new BufferedImage((int)img.getScaledWidth(),
borderWidth,
BufferedImage.TYPE_INT_RGB);
Graphics2D g2 = bufferedBottom.createGraphics();
g2.setBackground(color);
g2.clearRect(0, 0, bufferedBottom.getWidth(),bufferedBottom.getHeight());
Image bottom = Image.getInstance(bufferedImage2ByteArray(bufferedBottom));
bottom.setAbsolutePosition(img.getAbsoluteX(), img.getAbsoluteY()-bufferedTop.getHeight()/2);
BufferedImage bufferedLeft = new BufferedImage(borderWidth,
(int)img.getScaledHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D g3 = bufferedLeft.createGraphics();
g3.setBackground(color);
g3.clearRect(0, 0, bufferedLeft.getWidth(),bufferedLeft.getHeight());
Image left = Image.getInstance(bufferedImage2ByteArray(bufferedLeft));
left.setAbsolutePosition(img.getAbsoluteX()-bufferedLeft.getWidth()/2, img.getAbsoluteY());
BufferedImage bufferedRight = new BufferedImage(borderWidth,
(int)img.getScaledHeight(),
BufferedImage.TYPE_INT_RGB);
Graphics2D g4 = bufferedRight.createGraphics();
g4.setBackground(color);
g4.clearRect(0, 0, bufferedRight.getWidth(),bufferedRight.getHeight());
Image right = Image.getInstance(bufferedImage2ByteArray(bufferedRight));
right.setAbsolutePosition(img.getAbsoluteX()+img.getScaledWidth()-bufferedRight.getWidth()/2, img.getAbsoluteY());
doc.add(top);
doc.add(bottom);
doc.add(left);
doc.add(right);
}
catch (Exception e) {
e.printStackTrace();
}
}
private static byte[] bufferedImage2ByteArray(BufferedImage img) {
try {
ByteArrayOutputStream baos = new ByteArrayOutputStream(1000);
ImageIO.write(img, "png", baos); //$NON-NLS-1$
baos.flush();
byte[] result = baos.toByteArray();
baos.close();
return result;
}
catch(Exception e) {
e.printStackTrace();
return new byte[0];
}
}
}