/* * 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. */ /* $Id$ */ package org.apache.fop.render.rtf.rtflib.rtfdoc; /* * This file is part of the RTF library of the FOP project, which was originally * created by Bertrand Delacretaz <bdelacretaz@codeconsult.ch> and by other * contributors to the jfor project (www.jfor.org), who agreed to donate jfor to * the FOP project. */ import java.io.File; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.MalformedURLException; import java.net.URL; import org.apache.commons.io.IOUtils; import org.apache.fop.render.rtf.rtflib.tools.ImageConstants; import org.apache.fop.render.rtf.rtflib.tools.ImageUtil; /** * <p>Creates an RTF image from an external graphic file. * This class belongs to the <fo:external-graphic> tag processing.</p> * * <p>Supports relative path like "../test.gif", too (01-08-24)</p> * * <p>Limitations:</p> * <ul> * <li> Only the image types PNG, JPEG and EMF are supported * <li> The GIF is supported, too, but will be converted to JPG * <li> Only the attributes SRC (required), WIDTH, HEIGHT, SCALING are supported * <li> The SCALING attribute supports (uniform | non-uniform) * </ul> * * <p>Known Bugs:</p> * <ul> * <li> If the emf image has a desired size, the image will be clipped * <li> The emf, jpg & png image will not be displayed in correct size * </ul> * * <p>This work was authored by Andreas Putz (a.putz@skynamics.com) and * Gianugo Rabellino (gianugo@rabellino.it).</p> */ public class RtfExternalGraphic extends RtfElement { /** Exception thrown when an image file/URL cannot be read */ public static class ExternalGraphicException extends IOException { ExternalGraphicException(String reason) { super(reason); } } ////////////////////////////////////////////////// // Supported Formats ////////////////////////////////////////////////// private static class FormatBase { /** * Determines whether the image is in the according format. * * @param data Image * * @return * true If according type\n * false Other type */ public static boolean isFormat(byte[] data) { return false; } /** * Convert image data if necessary - for example when format is not supported by rtf. * * @param format Format type * @param data Image */ public FormatBase convert(FormatBase format, byte[] data) { return format; } /** * Determine image file format. * * @param data Image * * @return Image format class */ public static FormatBase determineFormat(byte[] data) { if (FormatPNG.isFormat(data)) { return new FormatPNG(); } else if (FormatJPG.isFormat(data)) { return new FormatJPG(); } else if (FormatEMF.isFormat(data)) { return new FormatEMF(); } else if (FormatGIF.isFormat(data)) { return new FormatGIF(); } else if (FormatBMP.isFormat(data)) { return new FormatBMP(); } else { return null; } } /** * Get image type. * * @return Image format class */ public int getType() { return ImageConstants.I_NOT_SUPPORTED; } /** * Get rtf tag. * * @return Rtf tag for image format. */ public String getRtfTag() { return ""; } } private static class FormatGIF extends FormatBase { public static boolean isFormat(byte[] data) { // Indentifier "GIF8" on position 0 byte [] pattern = new byte [] {(byte) 0x47, (byte) 0x49, (byte) 0x46, (byte) 0x38}; return ImageUtil.compareHexValues(pattern, data, 0, true); } public int getType() { return ImageConstants.I_GIF; } } private static class FormatEMF extends FormatBase { public static boolean isFormat(byte[] data) { // No offical Indentifier known byte [] pattern = new byte [] {(byte) 0x01, (byte) 0x00, (byte) 0x00}; return ImageUtil.compareHexValues(pattern, data, 0, true); } public int getType() { return ImageConstants.I_EMF; } public String getRtfTag() { return "emfblip"; } } private static class FormatBMP extends FormatBase { public static boolean isFormat(byte[] data) { byte [] pattern = new byte [] {(byte) 0x42, (byte) 0x4D}; return ImageUtil.compareHexValues(pattern, data, 0, true); } public int getType() { return ImageConstants.I_BMP; } } private static class FormatJPG extends FormatBase { public static boolean isFormat(byte[] data) { // Indentifier "0xFFD8" on position 0 byte [] pattern = new byte [] {(byte) 0xFF, (byte) 0xD8}; return ImageUtil.compareHexValues(pattern, data, 0, true); } public int getType() { return ImageConstants.I_JPG; } public String getRtfTag() { return "jpegblip"; } } private static class FormatPNG extends FormatBase { public static boolean isFormat(byte[] data) { // Indentifier "PNG" on position 1 byte [] pattern = new byte [] {(byte) 0x50, (byte) 0x4E, (byte) 0x47}; return ImageUtil.compareHexValues(pattern, data, 1, true); } public int getType() { return ImageConstants.I_PNG; } public String getRtfTag() { return "pngblip"; } } ////////////////////////////////////////////////// // @@ Members ////////////////////////////////////////////////// /** * The url of the image */ protected URL url; /** * The height of the image (in pixels) */ protected int height = -1; /** * The desired height (in twips) */ protected int heightDesired = -1; /** * Flag whether the desired height is a percentage */ protected boolean perCentH; /** * The width of the image (in pixels) */ protected int width = -1; /** * The desired width (in twips) */ protected int widthDesired = -1; /** * Flag whether the desired width is a percentage */ protected boolean perCentW; /** * Flag whether the image size shall be adjusted */ protected boolean scaleUniform; /** cropping on left/top/right/bottom edges for \piccrop*N */ private int[] cropValues = new int[4]; /** * Graphic compression rate */ protected int graphicCompressionRate = 80; /** The image data */ private byte[] imagedata; /** The image format */ private FormatBase imageformat; ////////////////////////////////////////////////// // @@ Construction ////////////////////////////////////////////////// /** * Default constructor. * Create an RTF element as a child of given container. * * @param container a <code>RtfContainer</code> value * @param writer a <code>Writer</code> value * @throws IOException for I/O problems */ public RtfExternalGraphic(RtfContainer container, Writer writer) throws IOException { super(container, writer); } /** * Default constructor. * * @param container a <code>RtfContainer</code> value * @param writer a <code>Writer</code> value * @param attributes a <code>RtfAttributes</code> value * @throws IOException for I/O problems */ public RtfExternalGraphic(RtfContainer container, Writer writer, RtfAttributes attributes) throws IOException { super(container, writer, attributes); } ////////////////////////////////////////////////// // @@ RtfElement implementation ////////////////////////////////////////////////// /** * RtfElement override - catches ExternalGraphicException and writes a warning * message to the document if image cannot be read * @throws IOException for I/O problems */ protected void writeRtfContent() throws IOException { try { writeRtfContentWithException(); } catch (ExternalGraphicException ie) { writeExceptionInRtf(ie); } } /** * Writes the RTF content to m_writer - this one throws ExternalGraphicExceptions * * @exception IOException On error */ protected void writeRtfContentWithException() throws IOException { if (writer == null) { return; } if (url == null && imagedata == null) { throw new ExternalGraphicException( "No image data is available (neither URL, nor in-memory)"); } String linkToRoot = System.getProperty("jfor_link_to_root"); if (url != null && linkToRoot != null) { writer.write("{\\field {\\* \\fldinst { INCLUDEPICTURE \""); writer.write(linkToRoot); File urlFile = new File(url.getFile()); writer.write(urlFile.getName()); writer.write("\" \\\\* MERGEFORMAT \\\\d }}}"); return; } // getRtfFile ().getLog ().logInfo ("Writing image '" + url + "'."); if (imagedata == null) { try { final InputStream in = url.openStream(); try { imagedata = IOUtils.toByteArray(url.openStream()); } finally { IOUtils.closeQuietly(in); } } catch (Exception e) { throw new ExternalGraphicException("The attribute 'src' of " + "<fo:external-graphic> has a invalid value: '" + url + "' (" + e + ")"); } } if (imagedata == null) { return; } // Determine image file format String file = (url != null ? url.getFile() : "<unknown>"); imageformat = FormatBase.determineFormat(imagedata); if (imageformat != null) { imageformat = imageformat.convert(imageformat, imagedata); } if (imageformat == null || imageformat.getType() == ImageConstants.I_NOT_SUPPORTED || "".equals(imageformat.getRtfTag())) { throw new ExternalGraphicException("The tag <fo:external-graphic> " + "does not support " + file.substring(file.lastIndexOf(".") + 1) + " - image type."); } // Writes the beginning of the rtf image writeGroupMark(true); writeStarControlWord("shppict"); writeGroupMark(true); writeControlWord("pict"); StringBuffer buf = new StringBuffer(imagedata.length * 3); writeControlWord(imageformat.getRtfTag()); computeImageSize(); writeSizeInfo(); writeAttributes(getRtfAttributes(), null); for (byte anImagedata : imagedata) { int iData = anImagedata; // Make positive byte if (iData < 0) { iData += 256; } if (iData < 16) { // Set leading zero and append buf.append('0'); } buf.append(Integer.toHexString(iData)); } int len = buf.length(); char[] chars = new char[len]; buf.getChars(0, len, chars, 0); writer.write(chars); // Writes the end of RTF image writeGroupMark(false); writeGroupMark(false); } private void computeImageSize() { if (imageformat.getType() == ImageConstants.I_PNG) { width = ImageUtil.getIntFromByteArray(imagedata, 16, 4, true); height = ImageUtil.getIntFromByteArray(imagedata, 20, 4, true); } else if (imageformat.getType() == ImageConstants.I_JPG) { int basis = -1; byte ff = (byte) 0xff; byte c0 = (byte) 0xc0; for (int i = 0; i < imagedata.length; i++) { byte b = imagedata[i]; if (b != ff) { continue; } if (i == imagedata.length - 1) { continue; } b = imagedata[i + 1]; if (b != c0) { continue; } basis = i + 5; break; } if (basis != -1) { width = ImageUtil.getIntFromByteArray(imagedata, basis + 2, 2, true); height = ImageUtil.getIntFromByteArray(imagedata, basis, 2, true); } } else if (imageformat.getType() == ImageConstants.I_EMF) { int i = 0; i = ImageUtil.getIntFromByteArray(imagedata, 151, 4, false); if (i != 0) { width = i; } i = ImageUtil.getIntFromByteArray(imagedata, 155, 4, false); if (i != 0) { height = i; } } } private void writeSizeInfo() throws IOException { // Set image size if (width != -1) { writeControlWord("picw" + width); } if (height != -1) { writeControlWord("pich" + height); } if (widthDesired != -1) { if (perCentW) { writeControlWord("picscalex" + widthDesired); } else { //writeControlWord("picscalex" + widthDesired * 100 / width); writeControlWord("picwgoal" + widthDesired); } } else if (scaleUniform && heightDesired != -1) { if (perCentH) { writeControlWord("picscalex" + heightDesired); } else { writeControlWord("picscalex" + heightDesired * 100 / height); } } if (heightDesired != -1) { if (perCentH) { writeControlWord("picscaley" + heightDesired); } else { //writeControlWord("picscaley" + heightDesired * 100 / height); writeControlWord("pichgoal" + heightDesired); } } else if (scaleUniform && widthDesired != -1) { if (perCentW) { writeControlWord("picscaley" + widthDesired); } else { writeControlWord("picscaley" + widthDesired * 100 / width); } } if (this.cropValues[0] != 0) { writeOneAttribute("piccropl", this.cropValues[0]); } if (this.cropValues[1] != 0) { writeOneAttribute("piccropt", this.cropValues[1]); } if (this.cropValues[2] != 0) { writeOneAttribute("piccropr", this.cropValues[2]); } if (this.cropValues[3] != 0) { writeOneAttribute("piccropb", this.cropValues[3]); } } ////////////////////////////////////////////////// // @@ Member access ////////////////////////////////////////////////// /** * Sets the desired height of the image. * * @param theHeight The desired image height (as a string in twips or as a percentage) */ public void setHeight(String theHeight) { this.heightDesired = ImageUtil.getInt(theHeight); this.perCentH = ImageUtil.isPercent(theHeight); } /** * Sets the desired width of the image. * * @param theWidth The desired image width (as a string in twips or as a percentage) */ public void setWidth(String theWidth) { this.widthDesired = ImageUtil.getInt(theWidth); this.perCentW = ImageUtil.isPercent(theWidth); } /** * Sets the desired width of the image. * @param twips The desired image width (in twips) */ public void setWidthTwips(int twips) { this.widthDesired = twips; this.perCentW = false; } /** * Sets the desired height of the image. * @param twips The desired image height (in twips) */ public void setHeightTwips(int twips) { this.heightDesired = twips; this.perCentH = false; } /** * Sets the flag whether the image size shall be adjusted. * * @param value * true image width or height shall be adjusted automatically\n * false no adjustment */ public void setScaling(String value) { setUniformScaling("uniform".equalsIgnoreCase(value)); } /** * Sets the flag whether the image size shall be adjusted. * * @param uniform * true image width or height shall be adjusted automatically\n * false no adjustment */ public void setUniformScaling(boolean uniform) { this.scaleUniform = uniform; } /** * Sets cropping values for all four edges for the \piccrop*N commands. * A positive value crops toward the center of the picture; * a negative value crops away from the center, adding a space border around the picture * @param left left cropping value (in twips) * @param top top cropping value (in twips) * @param right right cropping value (in twips) * @param bottom bottom cropping value (in twips) */ public void setCropping(int left, int top, int right, int bottom) { this.cropValues[0] = left; this.cropValues[1] = top; this.cropValues[2] = right; this.cropValues[3] = bottom; } /** * Sets the binary imagedata of the image. * * @param data binary imagedata as read from file. * @throws IOException On error */ public void setImageData(byte[] data) throws IOException { this.imagedata = data; } /** * Sets the url of the image. * * @param urlString Image url like "file://..." * @throws IOException On error */ public void setURL(String urlString) throws IOException { URL tmpUrl = null; try { tmpUrl = new URL(urlString); } catch (MalformedURLException e) { try { tmpUrl = new File(urlString).toURI().toURL(); } catch (MalformedURLException ee) { throw new ExternalGraphicException("The attribute 'src' of " + "<fo:external-graphic> has a invalid value: '" + urlString + "' (" + ee + ")"); } } this.url = tmpUrl; } /** * Gets the compression rate for the image in percent. * @return Compression rate */ public int getCompressionRate() { return graphicCompressionRate; } /** * Sets the compression rate for the image in percent. * * @param percent Compression rate * @return true if the compression rate is valid (0..100), false if invalid */ public boolean setCompressionRate(int percent) { if (percent < 1 || percent > 100) { return false; } graphicCompressionRate = percent; return true; } ////////////////////////////////////////////////// // @@ Helpers ////////////////////////////////////////////////// /** * @return true if this element would generate no "useful" RTF content */ public boolean isEmpty() { return url == null; } }