Hi Bruno, Hi Paulo,

We make extensive use of the Graphics2D interface to various destinations and 
discovered some issues with iText we intend to share.

First regarding the drawString(AttributedCharacterIterator):

Using iText I discovered that some Java TextAttributes are only partially 
implemented while others are implemented but have side effects.
Therefore I prepared a few improvement suggestions to the Graphics2D interface 
to be applied to iText if anyone is interested.


a) Regarding PDFGraphics2D.drawString(AttributedCharacterIterator):

First of all, compared to Java Graphics2D, iText 
drawString(AttributedCharacterIterator) is not stateless.

Thus, if I draw a String in a different Font or Color using TextAttributes, 
these change the state of PDFGraphics2D and carry on to subsequent regular 
drawStrings. The provided patch modifies the drawString method to reset the 
Font and text Color back to the values prior to calling the method between and 
after each attribute change. This resembles the behaviour of Sun's Graphics2D.

2) Furthermore, we added support for Textattribute.STRIKETHROUGH.

3) Mixing the TextAttribute.FONT with other font-manipulating attributes (e.g. 
FONTSIZE) previously could cause unpredictable behaviour.

For clarity about the problem:
The FONT textattribute defines a font object to be used for the drawstring 
operation and has precedence over other font-attributes like font size or font 
family.
Since not all VMs support FONT (e.g. the Apple VM for mac has problems here 
when printing) setting both FONTS together with other font attributes is 
sometimes necessary.

The current iText algorithm iterates over the TextAttributes and successively 
applies them in the order they are stored in the HashMap (which is unknown and 
might vary depending on HashMap size and implementation).
Thus if the order was first the FONT object and afterwards FONTSIZE, this 
resulted the new fonsize to be applied after the already explicitly defined 
font and thus in a fond different sized than the one Sun  would have used..

My patch fixes this by handling the FONT attribute first and ignoring any other 
font related attributes if a font has already been set.

4) Previously only a fixed set of font attributes (hardcoded into iText) have 
been supported. I suggest using the VM an passing all attributes given to the 
string before creating a new Font object. This allows all Attributes known by 
the VM to be applied to the font and reduces the need to maintain the code.

5) According to Sun TextAttribute.FOREGROUND accepts also Paint types, not only 
Color types. Currently a Paint type causes a ClassCastException.
   Besides now accepting Paints, I added instance checks to almost any value 
object before doing casts (thus ignoring wrong types and not aborting the 
method - as Sun does).

6) Same with Attributes having 'null' values instead of an object passed.



b) regarding Graphics2D.drawImage:

If drawImage is called multiple times with the same Java Image for each 
drawImage call a separate image object is created in the resulting PDF 
sometimes unnecessarily causing *huge* PDFs.
This situation could e.g. happen when drawing a logo image or such once on each 
page or even more when creating a tiling pattern using images (I know that then 
one should use PdfPatternPainter directly but this still occurs).
Thus after painting an image it would be useful to cache the PDFImage created 
inside PDFGraphics2D and reuse it next time.

Therefore I attached a second patch containg such an image cache a) reducing 
PDF size and b) reducing processing time when creating PDFs significantly.




Best regards,


Ludger Bünger

--

Ludger Bünger
Senior Software Engineer

RealObjects GmbH

Index: src/core/com/lowagie/text/pdf/PdfGraphics2D.java
===================================================================
--- src/core/com/lowagie/text/pdf/PdfGraphics2D.java    (revision 3922)
+++ src/core/com/lowagie/text/pdf/PdfGraphics2D.java    (working copy)
@@ -98,6 +98,7 @@
 import java.util.Hashtable;
 import java.util.Iterator;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
 
 import com.lowagie.text.pdf.internal.PolylineShape;
@@ -159,6 +160,7 @@
 
     // Added by Jurij Bilas
     protected boolean underline;          // indicates if the font style is 
underlined
+    private boolean strikeThrough;        // indicates if the font style is 
striked through
       
     protected PdfGState fillGState[] = new PdfGState[256];
     protected PdfGState strokeGState[] = new PdfGState[256];
@@ -300,54 +302,65 @@
      * @param iter
      */
     protected void doAttributes(AttributedCharacterIterator iter) {
-        underline = false;
-        Set set = iter.getAttributes().keySet();
+        
+        Font font = null;
+        Map fontAttributes = null;
+        
+        Map attributesMap = iter.getAttributes();
+        
+        // pre-process Font
+        Object value = attributesMap.get(TextAttribute.FONT);
+        if (value instanceof Font) {
+            font = (Font)value;
+        }
+        
+        Set set = attributesMap.entrySet();
         for(Iterator iterator = set.iterator(); iterator.hasNext();) {
-            AttributedCharacterIterator.Attribute attribute = 
(AttributedCharacterIterator.Attribute)iterator.next();
-            if (!(attribute instanceof TextAttribute))
+            Entry entry = (Entry)iterator.next();
+            Object attribute = entry.getKey();
+            value = entry.getValue();
+            
+            if (value == null
+                    || !(attribute instanceof TextAttribute)) {
                 continue;
+            }
+            
             TextAttribute textattribute = (TextAttribute)attribute;
             if(textattribute.equals(TextAttribute.FONT)) {
-                Font font = (Font)iter.getAttributes().get(textattribute);
-                setFont(font);
+                // skip font since this attribute has already been 
pre-processed
             }
             else if(textattribute.equals(TextAttribute.UNDERLINE)) {
-                if(iter.getAttributes().get(textattribute) == 
TextAttribute.UNDERLINE_ON)
-                    underline = true;
+                underline = (value == TextAttribute.UNDERLINE_ON);
             }
-            else if(textattribute.equals(TextAttribute.SIZE)) {
-                Object obj = iter.getAttributes().get(textattribute);
-                if(obj instanceof Integer) {
-                    int i = ((Integer)obj).intValue();
-                    setFont(getFont().deriveFont(getFont().getStyle(), i));
-                }
-                else if(obj instanceof Float) {
-                    float f = ((Float)obj).floatValue();
-                    setFont(getFont().deriveFont(getFont().getStyle(), f));
-                }
+            else if(textattribute.equals(TextAttribute.STRIKETHROUGH)) {
+                strikeThrough = (value == TextAttribute.STRIKETHROUGH_ON);
             }
             else if(textattribute.equals(TextAttribute.FOREGROUND)) {
-                setColor((Color) iter.getAttributes().get(textattribute));
-            }
-            else if(textattribute.equals(TextAttribute.FAMILY)) {
-              Font font = getFont();
-              Map fontAttributes = font.getAttributes();
-              fontAttributes.put(TextAttribute.FAMILY, 
iter.getAttributes().get(textattribute));
-              setFont(font.deriveFont(fontAttributes));
+                if (value instanceof Paint) { //also covers 'Color'
+                    setPaint((Paint)value);
+                }
             }
-            else if(textattribute.equals(TextAttribute.POSTURE)) {
-              Font font = getFont();
-              Map fontAttributes = font.getAttributes();
-              fontAttributes.put(TextAttribute.POSTURE, 
iter.getAttributes().get(textattribute));
-              setFont(font.deriveFont(fontAttributes)); 
+            else if(textattribute.equals(TextAttribute.BACKGROUND)) {
+                if (value instanceof Paint) { //also covers 'Color'
+                    // TODO: implement
+                }
             }
-            else if(textattribute.equals(TextAttribute.WEIGHT)) {
-              Font font = getFont();
-              Map fontAttributes = font.getAttributes();
-              fontAttributes.put(TextAttribute.WEIGHT, 
iter.getAttributes().get(textattribute));
-              setFont(font.deriveFont(fontAttributes)); 
+            // we assume that all textAttribute besides those caught above
+            // are font attributes
+            else if (font == null) { // only do font attribute processing if 
no font has been set by TextAttribute.FONT
+                
+                if (fontAttributes == null) {
+                    // initialize font attributes using graphics2D font
+                    fontAttributes = getFont().getAttributes();
+                }
+                fontAttributes.put(textattribute, value);
             }
         }
+        
+        if (fontAttributes != null) {
+            font = getFont().deriveFont(fontAttributes);
+        }
+        setFont(font);
     }
 
     /**
@@ -489,19 +502,34 @@
             if(underline)
             {
                 // These two are supposed to be taken from the .AFM file
-                //int UnderlinePosition = -100;
+                int UnderlinePosition = 50;
                 int UnderlineThickness = 50;
                 //
-                double d = asPoints(UnderlineThickness, (int)fontSize);
-                Stroke savedStroke = originalStroke;
-                setStroke(new BasicStroke((float)d));
-                y = (float)(y + asPoints(UnderlineThickness, (int)fontSize));
-                Line2D line = new Line2D.Double(x, y, width+x, y);
-                draw(line);
-                setStroke(savedStroke);
+                drawStringLine(x, y, width, UnderlinePosition, 
UnderlineThickness);
+            }
+            if(strikeThrough)
+            {
+                // These two are supposed to be taken from the .AFM file
+                int strikeLinePosition = 350;
+                int strikeLineThickness = 50;
+                
+                drawStringLine(x, y, width, strikeLinePosition, 
strikeLineThickness);
             }
         }
     }
+    
+    // introduced method to reuse code for drawing underline/strike through
+    private void drawStringLine(float x, float y, double width,
+            int linePosition, int lineThickness) {
+        double d = asPoints(lineThickness, (int)fontSize);
+        double p = asPoints (linePosition, (int)fontSize);
+        Stroke savedStroke = originalStroke;
+        setStroke(new BasicStroke((float)d));
+        y = (float)(y + asPoints(lineThickness, (int)fontSize));
+        Line2D line = new Line2D.Double(x, y-p, (width+x), y-p);
+        draw(line);
+        setStroke(savedStroke);
+    }
 
     /**
      * @see Graphics#drawString(AttributedCharacterIterator, int, int)
@@ -514,13 +542,11 @@
      * @see Graphics2D#drawString(AttributedCharacterIterator, float, float)
      */
     public void drawString(AttributedCharacterIterator iter, float x, float y) 
{
-/*
-        StringBuffer sb = new StringBuffer();
-        for(char c = iter.first(); c != AttributedCharacterIterator.DONE; c = 
iter.next()) {
-            sb.append(c);
-        }
-        drawString(sb.toString(),x,y);
-*/
+        
+        // store initial graphics2D state
+        Font originalFont = getFont();
+        Paint originalPaint = getPaint();
+        
         StringBuffer stringbuffer = new StringBuffer(iter.getEndIndex());
         for(char c = iter.first(); c != '\uFFFF'; c = iter.next())
         {
@@ -531,7 +557,9 @@
                     drawString(stringbuffer.toString(), x, y);
                     FontMetrics fontmetrics = getFontMetrics();
                     x = (float)(x + 
fontmetrics.getStringBounds(stringbuffer.toString(), this).getWidth());
-                    stringbuffer.delete(0, stringbuffer.length());
+                    stringbuffer.setLength(0);
+                    // reset previous doAttributes changes to graphics2D state 
prior to calling drawString 
+                    resetAttributedStates(originalFont, originalPaint);
                 }
                 doAttributes(iter);
             }
@@ -539,7 +567,17 @@
         }
         
         drawString(stringbuffer.toString(), x, y);
+        // reset doAttributes changes to graphics2D state prior to calling 
drawString 
+        resetAttributedStates(originalFont, originalPaint);
+    }
+    
+    // introduced method to undo all graphics2D state changes done by 
'doAttributes'
+    private void resetAttributedStates(Font originalFont, Paint originalPaint) 
{
+        // reset previous state
+        setFont(originalFont);
+        setPaint(originalPaint);
         underline = false;
+        strikeThrough = false;
     }
     
     /**
@@ -992,7 +1030,8 @@
             font = f;
             return;
         }
-        if (f == font)
+        // check for font equality not only identity
+        if (f.equals(font))
             return;
         font = f;
         fontSize = f.getSize2D();
Index: src/core/com/lowagie/text/pdf/PdfGraphics2D.java
===================================================================
--- src/core/com/lowagie/text/pdf/PdfGraphics2D.java    (revision 3922)
+++ src/core/com/lowagie/text/pdf/PdfGraphics2D.java    (working copy)
@@ -92,6 +92,7 @@
 import java.awt.image.WritableRaster;
 import java.awt.image.renderable.RenderableImage;
 import java.io.ByteArrayOutputStream;
+import java.io.IOException;
 import java.text.AttributedCharacterIterator;
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -99,7 +100,9 @@
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Set;
+import java.util.WeakHashMap;
 
+import com.lowagie.text.BadElementException;
 import com.lowagie.text.pdf.internal.PolylineShape;
 import java.util.Locale;
 import javax.imageio.IIOImage;
@@ -160,6 +163,8 @@
     // Added by Jurij Bilas
     protected boolean underline;          // indicates if the font style is 
underlined
       
+    private Map pdfImageCache = new WeakHashMap();
+    
     protected PdfGState fillGState[] = new PdfGState[256];
     protected PdfGState strokeGState[] = new PdfGState[256];
     protected int currentFillGState = 255;
@@ -912,6 +917,7 @@
         g2.strokeGState = this.strokeGState;
         g2.background = this.background;
         g2.mediaTracker = this.mediaTracker;
+        g2.pdfImageCache = this.pdfImageCache;
         g2.convertImagesToJPEG = this.convertImagesToJPEG;
         g2.jpegQuality = this.jpegQuality;
         g2.setFont(this.font);
@@ -1446,34 +1452,9 @@
         }
         
         try {
-            com.lowagie.text.Image image = null;
-            if(!convertImagesToJPEG){
-                image = com.lowagie.text.Image.getInstance(img, bgColor);
-            }
-            else{
-                BufferedImage scaled = new BufferedImage(img.getWidth(null), 
img.getHeight(null), BufferedImage.TYPE_INT_RGB);
-                Graphics2D g3 = scaled.createGraphics();
-                g3.drawImage(img, 0, 0, img.getWidth(null), 
img.getHeight(null), null);
-                g3.dispose();
-                
-                ByteArrayOutputStream baos = new ByteArrayOutputStream();
-                ImageWriteParam iwparam = new 
JPEGImageWriteParam(Locale.getDefault());
-                iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
-                iwparam.setCompressionQuality(jpegQuality);//Set here your 
compression rate
-                ImageWriter iw = 
(ImageWriter)ImageIO.getImageWritersByFormatName("jpg").next();
-                ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
-                iw.setOutput(ios);
-                iw.write(null, new IIOImage(scaled, null, null), iwparam);
-                iw.dispose();
-                ios.close();
-
-                scaled.flush();
-                scaled = null;
-                image = com.lowagie.text.Image.getInstance(baos.toByteArray());
-                
-            }
+            com.lowagie.text.Image image = getImageInstance(img, bgColor, 
convertImagesToJPEG, false);
             if (mask!=null) {
-                com.lowagie.text.Image msk = 
com.lowagie.text.Image.getInstance(mask, null, true);
+                com.lowagie.text.Image msk = getImageInstance(mask, true);
                 msk.makeMask();
                 msk.setInverted(true);
                 image.setImageMask(msk);
@@ -1514,6 +1495,122 @@
         }
     }
     
+    private final class ImageEntry {
+        
+        private final Map[] colorImageMaps = {null, null, null, null};
+        
+        private int getMapNumber(boolean jpg, boolean bw) {
+            if (jpg) {
+                if (bw) {
+                    return 3;
+                }
+                return 2;
+            }
+            if (bw) {
+                return 1;
+            }
+            return 0;
+        }
+        
+        com.lowagie.text.Image getImage(Color color, boolean jpg, boolean bw) {
+            Map colorMap = colorImageMaps[getMapNumber(jpg, bw)];
+            return colorMap != null
+                    ? (com.lowagie.text.Image)colorMap.get(color)
+                    : null;
+        }
+        
+        com.lowagie.text.Image putImage(com.lowagie.text.Image image, Color 
color, boolean jpg, boolean bw) {
+            com.lowagie.text.Image oldImage = getImage(color, false, bw);
+            Map colorImageMap = colorImageMaps[getMapNumber(jpg, bw)];
+            if (colorImageMap == null) {
+                colorImageMap = new HashMap();
+                colorImageMaps[getMapNumber(jpg, bw)] = colorImageMap;
+            }
+            colorImageMap.put(color, image);
+            return oldImage;
+        }
+    }
+    
+    /**
+     * Gets an instance of an Image from a java.awt.Image.
+     * 
+     * @param javaImage
+     *            the <CODE>java.awt.Image</CODE> to convert
+     * @param color
+     *            if different from <CODE>null</CODE> the transparency pixels
+     *            are replaced by this color
+     * @param convertToJPG
+     *            if <CODE>true</CODE> the image is converted to JPG
+     * @param forceBW
+     *            if <CODE>true</CODE> the image is treated as black and white
+     * @return an object of type <CODE>ImgRaw</CODE>
+     * @throws IOException 
+     * @throws BadElementException 
+     * @throws BadElementException
+     *             on error
+     * @throws IOException
+     *             on error
+     */
+    private com.lowagie.text.Image getImageInstance(Image javaImage, Color 
color,
+            boolean convertToJPG, boolean forceBW) throws BadElementException, 
IOException {
+        com.lowagie.text.Image pdfImage;
+        ImageEntry imageEntry = (ImageEntry)pdfImageCache.get(javaImage);
+        if (imageEntry != null) {
+            pdfImage = imageEntry.getImage(color, convertToJPG, forceBW);
+        } else {
+            imageEntry = new ImageEntry();
+            pdfImageCache.put(javaImage, imageEntry);
+            pdfImage = null;
+        }
+        
+        if (pdfImage == null) {
+            if (!convertToJPG) {
+                pdfImage = com.lowagie.text.Image.getInstance(javaImage, 
color, forceBW);
+            } else {
+                BufferedImage scaled = new 
BufferedImage(javaImage.getWidth(null), javaImage.getHeight(null), 
BufferedImage.TYPE_INT_RGB);
+                Graphics2D g3 = scaled.createGraphics();
+                g3.drawImage(javaImage, 0, 0, javaImage.getWidth(null), 
javaImage.getHeight(null), null);
+                g3.dispose();
+                
+                ByteArrayOutputStream baos = new ByteArrayOutputStream();
+                ImageWriteParam iwparam = new 
JPEGImageWriteParam(Locale.getDefault());
+                iwparam.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
+                iwparam.setCompressionQuality(jpegQuality);//Set here your 
compression rate
+                ImageWriter iw = 
(ImageWriter)ImageIO.getImageWritersByFormatName("jpg").next();
+                ImageOutputStream ios = ImageIO.createImageOutputStream(baos);
+                iw.setOutput(ios);
+                iw.write(null, new IIOImage(scaled, null, null), iwparam);
+                iw.dispose();
+                ios.close();
+                
+                scaled.flush();
+                scaled = null;
+                pdfImage = 
com.lowagie.text.Image.getInstance(baos.toByteArray());
+            }
+            imageEntry.putImage(pdfImage, color, convertToJPG, forceBW);
+        }
+        return pdfImage;
+    }
+    
+    /**
+     * Gets an instance of an Image from a java.awt.Image.
+     * 
+     * @param javaImage
+     *            the <CODE>java.awt.Image</CODE> to convert
+     * @param forceBW
+     *            if <CODE>true</CODE> the image is treated as black and white
+     * @return an object of type <CODE>ImgRaw</CODE>
+     * @throws IOException 
+     * @throws BadElementException 
+     * @throws BadElementException
+     *             on error
+     * @throws IOException
+     *             on error
+     */
+    private com.lowagie.text.Image getImageInstance(Image javaImage, boolean 
forceBW) throws BadElementException, IOException {
+        return getImageInstance(javaImage, null, false, forceBW);
+    }
+    
     private void setPaint(boolean invert, double xoffset, double yoffset, 
boolean fill) {
         if (paint instanceof Color) {
             Color color = (Color)paint;
@@ -1565,7 +1662,7 @@
                 TexturePaint tp = (TexturePaint)paint;
                 BufferedImage img = tp.getImage();
                 Rectangle2D rect = tp.getAnchorRect();
-                com.lowagie.text.Image image = 
com.lowagie.text.Image.getInstance(img, null);
+                com.lowagie.text.Image image = getImageInstance(img, false);
                 PdfPatternPainter pattern = cb.createPattern(image.getWidth(), 
image.getHeight());
                 AffineTransform inverse = this.normalizeMatrix();
                 inverse.translate(rect.getX(), rect.getY());
------------------------------------------------------------------------------
Register Now & Save for Velocity, the Web Performance & Operations 
Conference from O'Reilly Media. Velocity features a full day of 
expert-led, hands-on workshops and two days of sessions from industry 
leaders in dedicated Performance & Operations tracks. Use code vel09scf 
and Save an extra 15% before 5/3. http://p.sf.net/sfu/velocityconf
_______________________________________________
iText-questions mailing list
iText-questions@lists.sourceforge.net
https://lists.sourceforge.net/lists/listinfo/itext-questions

Buy the iText book: http://www.1t3xt.com/docs/book.php
Check the site with examples before you ask questions: 
http://www.1t3xt.info/examples/
You can also search the keywords list: http://1t3xt.info/tutorials/keywords/

Reply via email to