1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 
23 package com.sun.star.report.pentaho.output;
24 
25 import com.sun.star.report.ImageService;
26 import com.sun.star.report.InputRepository;
27 import com.sun.star.report.OutputRepository;
28 import com.sun.star.report.ReportExecutionException;
29 import com.sun.star.report.pentaho.DefaultNameGenerator;
30 
31 import java.awt.Dimension;
32 import java.awt.Image;
33 
34 import java.io.BufferedInputStream;
35 import java.io.ByteArrayInputStream;
36 import java.io.ByteArrayOutputStream;
37 import java.io.IOException;
38 import java.io.InputStream;
39 import java.io.OutputStream;
40 
41 import java.net.MalformedURLException;
42 import java.net.URI;
43 import java.net.URISyntaxException;
44 import java.net.URL;
45 import java.net.URLConnection;
46 
47 import java.sql.Blob;
48 import java.sql.SQLException;
49 
50 import java.util.Arrays;
51 import java.util.HashMap;
52 import java.util.Map;
53 import java.util.logging.Level;
54 import java.util.logging.Logger;
55 
56 import org.apache.commons.logging.Log;
57 import org.apache.commons.logging.LogFactory;
58 
59 import org.jfree.layouting.input.style.values.CSSNumericType;
60 import org.jfree.layouting.input.style.values.CSSNumericValue;
61 
62 import org.pentaho.reporting.libraries.base.util.IOUtils;
63 import org.pentaho.reporting.libraries.base.util.PngEncoder;
64 import org.pentaho.reporting.libraries.base.util.WaitingImageObserver;
65 
66 
67 /**
68  * This class manages the images embedded in a report.
69  *
70  * @author Thomas Morgner
71  * @since 31.03.2007
72  */
73 public class ImageProducer
74 {
75 
76     private static final Log LOGGER = LogFactory.getLog(ImageProducer.class);
77 
78     public static class OfficeImage
79     {
80 
81         private final CSSNumericValue width;
82         private final CSSNumericValue height;
83         private final String embeddableLink;
84 
OfficeImage(final String embeddableLink, final CSSNumericValue width, final CSSNumericValue height)85         public OfficeImage(final String embeddableLink, final CSSNumericValue width, final CSSNumericValue height)
86         {
87             this.embeddableLink = embeddableLink;
88             this.width = width;
89             this.height = height;
90         }
91 
getWidth()92         public CSSNumericValue getWidth()
93         {
94             return width;
95         }
96 
getHeight()97         public CSSNumericValue getHeight()
98         {
99             return height;
100         }
101 
getEmbeddableLink()102         public String getEmbeddableLink()
103         {
104             return embeddableLink;
105         }
106     }
107 
108     private static class ByteDataImageKey
109     {
110 
111         private final byte[] keyData;
112         private Integer hashCode;
113 
ByteDataImageKey(final byte[] keyData)114         protected ByteDataImageKey(final byte[] keyData)
115         {
116             if (keyData == null)
117             {
118                 throw new NullPointerException();
119             }
120             this.keyData = keyData;
121         }
122 
equals(final Object o)123         public boolean equals(final Object o)
124         {
125             if (this != o)
126             {
127                 if (o == null || getClass() != o.getClass())
128                 {
129                     return false;
130                 }
131 
132                 final ByteDataImageKey key = (ByteDataImageKey) o;
133                 if (!Arrays.equals(keyData, key.keyData))
134                 {
135                     return false;
136                 }
137             }
138 
139             return true;
140         }
141 
hashCode()142         public int hashCode()
143         {
144             if (hashCode != null)
145             {
146                 return hashCode;
147             }
148 
149             final int length = Math.min(keyData.length, 512);
150             int hashValue = 0;
151             for (int i = 0; i < length; i++)
152             {
153                 final byte b = keyData[i];
154                 hashValue = b + hashValue * 23;
155             }
156             this.hashCode = hashValue;
157             return hashValue;
158         }
159     }
160     private final Map imageCache;
161     private final InputRepository inputRepository;
162     private final OutputRepository outputRepository;
163     private final ImageService imageService;
164 
ImageProducer(final InputRepository inputRepository, final OutputRepository outputRepository, final ImageService imageService)165     public ImageProducer(final InputRepository inputRepository,
166             final OutputRepository outputRepository,
167             final ImageService imageService)
168     {
169         if (inputRepository == null)
170         {
171             throw new NullPointerException();
172         }
173         if (outputRepository == null)
174         {
175             throw new NullPointerException();
176         }
177         if (imageService == null)
178         {
179             throw new NullPointerException();
180         }
181 
182         this.inputRepository = inputRepository;
183         this.outputRepository = outputRepository;
184         this.imageService = imageService;
185         this.imageCache = new HashMap();
186     }
187 
188     /**
189      * Image-Data can be one of the following types: String, URL, URI, byte-array, blob.
190      *
191      * @param imageData
192      * @param preserveIRI
193      * @return
194      */
produceImage(final Object imageData, final boolean preserveIRI)195     public OfficeImage produceImage(final Object imageData,
196             final boolean preserveIRI)
197     {
198 
199         LOGGER.debug("Want to produce image " + imageData);
200         if (imageData instanceof String)
201         {
202             return produceFromString((String) imageData, preserveIRI);
203         }
204 
205         if (imageData instanceof URL)
206         {
207             return produceFromURL((URL) imageData, preserveIRI);
208         }
209 
210         if (imageData instanceof Blob)
211         {
212             return produceFromBlob((Blob) imageData);
213         }
214 
215         if (imageData instanceof byte[])
216         {
217             return produceFromByteArray((byte[]) imageData);
218         }
219 
220         if (imageData instanceof Image)
221         {
222             return produceFromImage((Image) imageData);
223         }
224         // not usable ..
225         return null;
226     }
227 
produceFromImage(final Image image)228     private OfficeImage produceFromImage(final Image image)
229     {
230         // quick caching ... use a weak list ...
231         final WaitingImageObserver obs = new WaitingImageObserver(image);
232         obs.waitImageLoaded();
233 
234         final PngEncoder encoder = new PngEncoder(image, PngEncoder.ENCODE_ALPHA, PngEncoder.FILTER_NONE, 5);
235         final byte[] data = encoder.pngEncode();
236         return produceFromByteArray(data);
237     }
238 
produceFromBlob(final Blob blob)239     private OfficeImage produceFromBlob(final Blob blob)
240     {
241         try
242         {
243             final InputStream inputStream = blob.getBinaryStream();
244             final int length = (int) blob.length();
245 
246             final ByteArrayOutputStream bout = new ByteArrayOutputStream(length);
247             try
248             {
249                 IOUtils.getInstance().copyStreams(inputStream, bout);
250             } finally
251             {
252                 inputStream.close();
253             }
254             return produceFromByteArray(bout.toByteArray());
255         }
256         catch (IOException e)
257         {
258             LOGGER.warn("Failed to produce image from Blob", e);
259         }
260         catch (SQLException e)
261         {
262             LOGGER.warn("Failed to produce image from Blob", e);
263         }
264         return null;
265     }
266 
produceFromByteArray(final byte[] data)267     private OfficeImage produceFromByteArray(final byte[] data)
268     {
269         final ByteDataImageKey imageKey = new ByteDataImageKey(data);
270         final OfficeImage o = (OfficeImage) imageCache.get(imageKey);
271         if (o != null)
272         {
273             return o;
274         }
275 
276         try
277         {
278             final String mimeType = imageService.getMimeType(data);
279             final Dimension dims = imageService.getImageSize(data);
280 
281             // copy the image into the local output-storage
282             // todo: Implement data-fingerprinting so that we can detect the mime-type
283             final OutputRepository storage = outputRepository.openOutputRepository("Pictures", null);
284             final DefaultNameGenerator nameGenerator = new DefaultNameGenerator(storage);
285             final String name = nameGenerator.generateName("image", mimeType);
286             final OutputStream outputStream = storage.createOutputStream(name, mimeType);
287             final ByteArrayInputStream bin = new ByteArrayInputStream(data);
288 
289             try
290             {
291                 IOUtils.getInstance().copyStreams(bin, outputStream);
292             } finally
293             {
294                 outputStream.close();
295                 storage.closeOutputRepository();
296             }
297 
298             final CSSNumericValue widthVal = CSSNumericValue.createValue(CSSNumericType.MM, dims.getWidth() / 100.0);
299             final CSSNumericValue heightVal = CSSNumericValue.createValue(CSSNumericType.MM, dims.getHeight() / 100.0);
300             final OfficeImage officeImage = new OfficeImage("Pictures/" + name, widthVal, heightVal);
301             imageCache.put(imageKey, officeImage);
302             return officeImage;
303         }
304         catch (IOException e)
305         {
306             LOGGER.warn("Failed to load image from local input-repository", e);
307         }
308         catch (ReportExecutionException e)
309         {
310             LOGGER.warn("Failed to create image from local input-repository", e);
311         }
312         return null;
313     }
314 
produceFromString(final String source, final boolean preserveIRI)315     private OfficeImage produceFromString(final String source,
316             final boolean preserveIRI)
317     {
318 
319         try
320         {
321             final URL url = new URL(source);
322             return produceFromURL(url, preserveIRI);
323         }
324         catch (MalformedURLException e)
325         {
326             // ignore .. but we had to try this ..
327         }
328 
329         final OfficeImage o = (OfficeImage) imageCache.get(source);
330         if (o != null)
331         {
332             return o;
333         }
334 
335         // Next, check whether this is a local path.
336         if (inputRepository.isReadable(source))
337         {
338             // cool, the file exists. Let's try to read it.
339             try
340             {
341                 final ByteArrayOutputStream bout = new ByteArrayOutputStream(8192);
342                 final InputStream inputStream = inputRepository.createInputStream(source);
343                 try
344                 {
345                     IOUtils.getInstance().copyStreams(inputStream, bout);
346                 } finally
347                 {
348                     inputStream.close();
349                 }
350                 final byte[] data = bout.toByteArray();
351                 final Dimension dims = imageService.getImageSize(data);
352                 final String mimeType = imageService.getMimeType(data);
353 
354                 final CSSNumericValue widthVal = CSSNumericValue.createValue(CSSNumericType.MM, dims.getWidth() / 100.0);
355                 final CSSNumericValue heightVal = CSSNumericValue.createValue(CSSNumericType.MM, dims.getHeight() / 100.0);
356 
357                 final String filename = copyToOutputRepository(mimeType, data);
358                 final OfficeImage officeImage = new OfficeImage(filename, widthVal, heightVal);
359                 imageCache.put(source, officeImage);
360                 return officeImage;
361             }
362             catch (IOException e)
363             {
364                 LOGGER.warn("Failed to load image from local input-repository", e);
365             }
366             catch (ReportExecutionException e)
367             {
368                 LOGGER.warn("Failed to create image from local input-repository", e);
369             }
370         }
371         else
372         {
373             try
374             {
375                 URI rootURI = new URI(inputRepository.getRootURL());
376                 final URI uri = rootURI.resolve(source);
377                 return produceFromURL(uri.toURL(), preserveIRI);
378             }
379             catch (URISyntaxException ex)
380             {
381             }
382             catch (MalformedURLException e)
383             {
384                 // ignore .. but we had to try this ..
385             }
386         }
387 
388         // Return the image as broken image instead ..
389         final OfficeImage officeImage = new OfficeImage(source, null, null);
390         imageCache.put(source, officeImage);
391         return officeImage;
392     }
393 
produceFromURL(final URL url, final boolean preserveIRI)394     private OfficeImage produceFromURL(final URL url,
395             final boolean preserveIRI)
396     {
397         final String urlString = url.toString();
398         URI uri = null;
399         try
400         {
401             uri = new URI(urlString);
402         }
403         catch (URISyntaxException ex)
404         {
405             Logger.getLogger(ImageProducer.class.getName()).log(Level.SEVERE, null, ex);
406         }
407         final OfficeImage o = (OfficeImage) imageCache.get(uri);
408         if (o != null)
409         {
410             return o;
411         }
412 
413         try
414         {
415             final ByteArrayOutputStream bout = new ByteArrayOutputStream(8192);
416             final URLConnection urlConnection = url.openConnection();
417             final InputStream inputStream = new BufferedInputStream(urlConnection.getInputStream());
418             try
419             {
420                 IOUtils.getInstance().copyStreams(inputStream, bout);
421             } finally
422             {
423                 inputStream.close();
424             }
425             final byte[] data = bout.toByteArray();
426 
427             final Dimension dims = imageService.getImageSize(data);
428             final String mimeType = imageService.getMimeType(data);
429             final CSSNumericValue widthVal = CSSNumericValue.createValue(CSSNumericType.MM, dims.getWidth() / 100.0);
430             final CSSNumericValue heightVal = CSSNumericValue.createValue(CSSNumericType.MM, dims.getHeight() / 100.0);
431 
432             if (preserveIRI)
433             {
434                 final OfficeImage retval = new OfficeImage(urlString, widthVal, heightVal);
435                 imageCache.put(uri, retval);
436                 return retval;
437             }
438 
439             final String name = copyToOutputRepository(mimeType, data);
440             final OfficeImage officeImage = new OfficeImage(name, widthVal, heightVal);
441             imageCache.put(uri, officeImage);
442             return officeImage;
443         }
444         catch (IOException e)
445         {
446             LOGGER.warn("Failed to load image from local input-repository" + e);
447         }
448         catch (ReportExecutionException e)
449         {
450             LOGGER.warn("Failed to create image from local input-repository" + e);
451         }
452 
453         if (!preserveIRI)
454         {
455             final OfficeImage image = new OfficeImage(urlString, null, null);
456             imageCache.put(uri, image);
457             return image;
458         }
459 
460         // OK, everything failed; the image is not - repeat it - not usable.
461         return null;
462     }
463 
copyToOutputRepository(final String urlMimeType, final byte[] data)464     private String copyToOutputRepository(final String urlMimeType, final byte[] data)
465             throws IOException, ReportExecutionException
466     {
467         final String mimeType;
468         if (urlMimeType == null)
469         {
470             mimeType = imageService.getMimeType(data);
471         }
472         else
473         {
474             mimeType = urlMimeType;
475         }
476 
477         // copy the image into the local output-storage
478         final OutputRepository storage = outputRepository.openOutputRepository("Pictures", null);
479         final DefaultNameGenerator nameGenerator = new DefaultNameGenerator(storage);
480         final String name = nameGenerator.generateName("image", mimeType);
481         final OutputStream outputStream = storage.createOutputStream(name, mimeType);
482         final ByteArrayInputStream bin = new ByteArrayInputStream(data);
483 
484         try
485         {
486             IOUtils.getInstance().copyStreams(bin, outputStream);
487         } finally
488         {
489             outputStream.close();
490             storage.closeOutputRepository();
491         }
492         return "Pictures/" + name;
493     }
494 }
495