- Fixed positioning of Java2D-based images (when GOCA is enabled). GraphicsDataDescriptor had a bit order bug. The Graphics2D image handler didn't save state and reposition the image origin. - Switched bitmap image handling in AFPGraphics2D to (re-)use AFPImageHandlerRenderedImage so it can profit from it's advanced image conversion functionality. This also avoids some bugs with certain image formats. - Added enhanced dithering functionality for images that need to be converted to bi-level images. git-svn-id: https://svn.apache.org/repos/asf/xmlgraphics/fop/trunk@953952 13f79535-47bb-0310-9956-ffa450edef68pull/37/head
latest features. We're trying to make AFP output work in as many environments as possible. | latest features. We're trying to make AFP output work in as many environments as possible. | ||||
However, to make AFP output work on older environments it is recommended to set to | However, to make AFP output work on older environments it is recommended to set to | ||||
configuration to 1 bit per pixel (see below on how to do this). In this case, all images | configuration to 1 bit per pixel (see below on how to do this). In this case, all images | ||||
are converted to bi-level images using IOCA function set 10 (FS10). If a higher number of | |||||
bits per pixel is configured, FOP has to switch to at least FS11 which may not work | |||||
everywhere. | |||||
are converted to bi-level images using IOCA function set 10 (FS10) and are enclosed in | |||||
page-segments since some implementation cannot deal with IOCA objects directly. | |||||
If a higher number of bits per pixel is configured, FOP has to switch to at least FS11 | |||||
which may not work everywhere. | |||||
</p> | </p> | ||||
</section> | </section> | ||||
<section id="afp-configuration"> | <section id="afp-configuration"> | ||||
colors. This will only have an effect if the color mode is set to "color". Example: | colors. This will only have an effect if the color mode is set to "color". Example: | ||||
</p> | </p> | ||||
<source><![CDATA[ | <source><![CDATA[ | ||||
<images mode="color" cmyk="true"/> | |||||
]]></source> | |||||
<images mode="color" cmyk="true"/>]]></source> | |||||
<p> | |||||
When the color mode is set to 1 bit (bi-level), the "dithering-quality" attribute can | |||||
be used to select the level of quality to use when converting images to bi-level images. | |||||
Valid values for this attribute are floating point numbers from 0.0 (fastest) to | |||||
1.0 (best), or special values: "minimum" (=0.0), "maximum" (1.0), | |||||
"medium" (0.5, the default). For the higher settings to work as expected, JAI needs to | |||||
be present in the classpath. If JAI is present, 0.0 results in a minimal darkness-level | |||||
switching between white and black. 0.5 does bayer-based dithering and 1.0 will use | |||||
error-diffusion dithering. The higher the value, the higher the quality and the slower | |||||
the processing of the images. | |||||
</p> | |||||
<source><![CDATA[ | |||||
<images mode="b+w" bits-per-pixel="1" dithering-quality="maximum"/>]]></source> | |||||
</section> | </section> | ||||
<section id="afp-shading-config"> | <section id="afp-shading-config"> | ||||
<title>Shading</title> | <title>Shading</title> |
import java.awt.image.renderable.RenderableImage; | import java.awt.image.renderable.RenderableImage; | ||||
import java.io.IOException; | import java.io.IOException; | ||||
import org.apache.commons.io.output.ByteArrayOutputStream; | |||||
import org.apache.commons.logging.Log; | import org.apache.commons.logging.Log; | ||||
import org.apache.commons.logging.LogFactory; | import org.apache.commons.logging.LogFactory; | ||||
import org.apache.xmlgraphics.java2d.GraphicContext; | import org.apache.xmlgraphics.java2d.GraphicContext; | ||||
import org.apache.xmlgraphics.java2d.StrokingTextHandler; | import org.apache.xmlgraphics.java2d.StrokingTextHandler; | ||||
import org.apache.xmlgraphics.java2d.TextHandler; | import org.apache.xmlgraphics.java2d.TextHandler; | ||||
import org.apache.xmlgraphics.ps.ImageEncodingHelper; | |||||
import org.apache.xmlgraphics.util.MimeConstants; | |||||
import org.apache.xmlgraphics.util.UnitConv; | import org.apache.xmlgraphics.util.UnitConv; | ||||
import org.apache.fop.afp.goca.GraphicsSetLineType; | import org.apache.fop.afp.goca.GraphicsSetLineType; | ||||
import org.apache.fop.afp.svg.AFPGraphicsConfiguration; | import org.apache.fop.afp.svg.AFPGraphicsConfiguration; | ||||
import org.apache.fop.afp.util.CubicBezierApproximator; | import org.apache.fop.afp.util.CubicBezierApproximator; | ||||
import org.apache.fop.fonts.FontInfo; | import org.apache.fop.fonts.FontInfo; | ||||
import org.apache.fop.render.afp.AFPImageHandlerRenderedImage; | |||||
import org.apache.fop.render.afp.AFPRenderingContext; | |||||
import org.apache.fop.svg.NativeImageHandler; | import org.apache.fop.svg.NativeImageHandler; | ||||
/** | /** | ||||
BufferedImage.TYPE_INT_ARGB); | BufferedImage.TYPE_INT_ARGB); | ||||
} | } | ||||
private AFPImageObjectInfo createImageObjectInfo( | |||||
RenderedImage img, int x, int y, int width, int height) throws IOException { | |||||
ImageInfo imageInfo = new ImageInfo(null, "image/unknown"); | |||||
ImageSize size = new ImageSize(img.getWidth(), img.getHeight(), 72); | |||||
imageInfo.setSize(size); | |||||
ImageRendered imageRendered = new ImageRendered(imageInfo, img, null); | |||||
RenderedImage renderedImage = imageRendered.getRenderedImage(); | |||||
// create image object info | |||||
AFPImageObjectInfo imageObjectInfo = new AFPImageObjectInfo(); | |||||
imageObjectInfo.setMimeType(MimeConstants.MIME_AFP_IOCA_FS45); | |||||
int bitsPerPixel = paintingState.getBitsPerPixel(); | |||||
imageObjectInfo.setBitsPerPixel(bitsPerPixel); | |||||
imageObjectInfo.setResourceInfo(resourceInfo); | |||||
int dataHeight = renderedImage.getHeight(); | |||||
imageObjectInfo.setDataHeight(dataHeight); | |||||
int dataWidth = renderedImage.getWidth(); | |||||
imageObjectInfo.setDataWidth(dataWidth); | |||||
int resolution = paintingState.getResolution(); | |||||
imageObjectInfo.setDataWidthRes(resolution); | |||||
imageObjectInfo.setDataHeightRes(resolution); | |||||
boolean colorImages = paintingState.isColorImages(); | |||||
imageObjectInfo.setColor(colorImages); | |||||
ByteArrayOutputStream boas = new ByteArrayOutputStream(); | |||||
ImageEncodingHelper.encodeRenderedImageAsRGB(renderedImage, boas); | |||||
byte[] imageData = boas.toByteArray(); | |||||
// convert to grayscale | |||||
if (!colorImages) { | |||||
boas.reset(); | |||||
imageObjectInfo.setBitsPerPixel(bitsPerPixel); | |||||
ImageEncodingHelper.encodeRGBAsGrayScale( | |||||
imageData, dataWidth, dataHeight, bitsPerPixel, boas); | |||||
imageData = boas.toByteArray(); | |||||
if (bitsPerPixel == 1) { | |||||
//FS10 should generate a page seqment to avoid problems | |||||
imageObjectInfo.setCreatePageSegment(true); | |||||
} | |||||
} | |||||
imageObjectInfo.setData(imageData); | |||||
if (imageInfo != null) { | |||||
imageObjectInfo.setUri(imageInfo.getOriginalURI()); | |||||
} | |||||
// create object area info | |||||
AFPObjectAreaInfo objectAreaInfo = new AFPObjectAreaInfo(); | |||||
objectAreaInfo.setX(x); | |||||
objectAreaInfo.setY(y); | |||||
objectAreaInfo.setWidth(width); | |||||
objectAreaInfo.setHeight(height); | |||||
objectAreaInfo.setWidthRes(resolution); | |||||
objectAreaInfo.setHeightRes(resolution); | |||||
imageObjectInfo.setObjectAreaInfo(objectAreaInfo); | |||||
return imageObjectInfo; | |||||
} | |||||
/** | /** | ||||
* Draws an AWT image into a BufferedImage using an AWT Graphics2D implementation | * Draws an AWT image into a BufferedImage using an AWT Graphics2D implementation | ||||
* | * | ||||
/** {@inheritDoc} */ | /** {@inheritDoc} */ | ||||
public boolean drawImage(Image img, int x, int y, int width, int height, | public boolean drawImage(Image img, int x, int y, int width, int height, | ||||
ImageObserver observer) { | ImageObserver observer) { | ||||
// draw with AWT Graphics2D | // draw with AWT Graphics2D | ||||
Dimension imageSize = new Dimension(width, height); | Dimension imageSize = new Dimension(width, height); | ||||
BufferedImage bufferedImage = buildBufferedImage(imageSize); | BufferedImage bufferedImage = buildBufferedImage(imageSize); | ||||
int imgWidth = img.getWidth(); | int imgWidth = img.getWidth(); | ||||
int imgHeight = img.getHeight(); | int imgHeight = img.getHeight(); | ||||
AffineTransform at = paintingState.getData().getTransform(); | |||||
AffineTransform gat = gc.getTransform(); | AffineTransform gat = gc.getTransform(); | ||||
int graphicsObjectHeight | int graphicsObjectHeight | ||||
= graphicsObj.getObjectEnvironmentGroup().getObjectAreaDescriptor().getHeight(); | = graphicsObj.getObjectEnvironmentGroup().getObjectAreaDescriptor().getHeight(); | ||||
int x = (int)Math.round(at.getTranslateX() + gat.getTranslateX()); | |||||
int y = (int)Math.round(at.getTranslateY() - (gat.getTranslateY() - graphicsObjectHeight)); | |||||
int width = (int)Math.round(imgWidth * gat.getScaleX()); | |||||
int height = (int)Math.round(imgHeight * -gat.getScaleY()); | |||||
double toMillipointFactor = UnitConv.IN2PT * 1000 / (double)paintingState.getResolution(); | |||||
double x = gat.getTranslateX(); | |||||
double y = -(gat.getTranslateY() - graphicsObjectHeight); | |||||
x = toMillipointFactor * x; | |||||
y = toMillipointFactor * y; | |||||
double w = toMillipointFactor * imgWidth * gat.getScaleX(); | |||||
double h = toMillipointFactor * imgHeight * -gat.getScaleY(); | |||||
AFPImageHandlerRenderedImage handler = new AFPImageHandlerRenderedImage(); | |||||
ImageInfo imageInfo = new ImageInfo(null, null); | |||||
imageInfo.setSize(new ImageSize( | |||||
img.getWidth(), img.getHeight(), paintingState.getResolution())); | |||||
imageInfo.getSize().calcSizeFromPixels(); | |||||
ImageRendered red = new ImageRendered(imageInfo, img, null); | |||||
Rectangle targetPos = new Rectangle( | |||||
(int)Math.round(x), | |||||
(int)Math.round(y), | |||||
(int)Math.round(w), | |||||
(int)Math.round(h)); | |||||
AFPRenderingContext context = new AFPRenderingContext(null, | |||||
resourceManager, paintingState, fontInfo, null); | |||||
try { | try { | ||||
// get image object info | |||||
AFPImageObjectInfo imageObjectInfo | |||||
= createImageObjectInfo(img, x, y, width, height); | |||||
// create image resource | |||||
resourceManager.createObject(imageObjectInfo); | |||||
handler.handleImage(context, red, targetPos); | |||||
} catch (IOException ioe) { | } catch (IOException ioe) { | ||||
handleIOException(ioe); | handleIOException(ioe); | ||||
} | } |
/** color image support */ | /** color image support */ | ||||
private boolean colorImages = false; | private boolean colorImages = false; | ||||
/** dithering quality setting (0.0f..1.0f) */ | |||||
private float ditheringQuality; | |||||
/** color image handler */ | /** color image handler */ | ||||
private ColorConverter colorConverter = GrayScaleColorConverter.getInstance(); | private ColorConverter colorConverter = GrayScaleColorConverter.getInstance(); | ||||
return this.cmykImagesSupported; | return this.cmykImagesSupported; | ||||
} | } | ||||
/** | |||||
* Gets the dithering quality setting to use when converting images to monochrome images. | |||||
* @return the dithering quality (a value between 0.0f and 1.0f) | |||||
*/ | |||||
public float getDitheringQuality() { | |||||
return this.ditheringQuality; | |||||
} | |||||
/** | |||||
* Sets the dithering quality setting to use when converting images to monochrome images. | |||||
* @param quality Defines the desired quality level for the conversion. | |||||
* Valid values: a value between 0.0f (fastest) and 1.0f (best) | |||||
*/ | |||||
public void setDitheringQuality(float quality) { | |||||
quality = Math.max(quality, 0.0f); | |||||
quality = Math.min(quality, 1.0f); | |||||
this.ditheringQuality = quality; | |||||
} | |||||
/** | /** | ||||
* Sets the output/device resolution | * Sets the output/device resolution | ||||
* | * |
return data; | return data; | ||||
} | } | ||||
private static final int ABS = 2; | |||||
private static final int IMGRES = 8; | |||||
private static final int ABS = 64; | |||||
private static final int IMGRES = 16; | |||||
/** | /** | ||||
* Returns the window specification data | * Returns the window specification data |
*/ | */ | ||||
void setShadingMode(AFPShadingMode shadingMode); | void setShadingMode(AFPShadingMode shadingMode); | ||||
/** | |||||
* Sets the dithering quality setting to use when converting images to monochrome images. | |||||
* @param quality Defines the desired quality level for the conversion. | |||||
* Valid values: a value between 0.0f (fastest) and 1.0f (best) | |||||
*/ | |||||
void setDitheringQuality(float quality); | |||||
/** | /** | ||||
* Sets the output/device resolution | * Sets the output/device resolution | ||||
* | * |
import org.apache.fop.render.afp.extensions.AFPElementMapping; | import org.apache.fop.render.afp.extensions.AFPElementMapping; | ||||
import org.apache.fop.render.afp.extensions.AFPIncludeFormMap; | import org.apache.fop.render.afp.extensions.AFPIncludeFormMap; | ||||
import org.apache.fop.render.afp.extensions.AFPInvokeMediumMap; | import org.apache.fop.render.afp.extensions.AFPInvokeMediumMap; | ||||
import org.apache.fop.render.afp.extensions.AFPPageSetup; | |||||
import org.apache.fop.render.afp.extensions.AFPPageOverlay; | import org.apache.fop.render.afp.extensions.AFPPageOverlay; | ||||
import org.apache.fop.render.afp.extensions.AFPPageSetup; | |||||
import org.apache.fop.render.intermediate.AbstractBinaryWritingIFDocumentHandler; | import org.apache.fop.render.intermediate.AbstractBinaryWritingIFDocumentHandler; | ||||
import org.apache.fop.render.intermediate.IFDocumentHandler; | import org.apache.fop.render.intermediate.IFDocumentHandler; | ||||
import org.apache.fop.render.intermediate.IFDocumentHandlerConfigurator; | import org.apache.fop.render.intermediate.IFDocumentHandlerConfigurator; | ||||
paintingState.setCMYKImagesSupported(value); | paintingState.setCMYKImagesSupported(value); | ||||
} | } | ||||
/** {@inheritDoc} */ | |||||
public void setDitheringQuality(float quality) { | |||||
this.paintingState.setDitheringQuality(quality); | |||||
} | |||||
/** {@inheritDoc} */ | /** {@inheritDoc} */ | ||||
public void setShadingMode(AFPShadingMode shadingMode) { | public void setShadingMode(AFPShadingMode shadingMode) { | ||||
this.shadingMode = shadingMode; | this.shadingMode = shadingMode; |
package org.apache.fop.render.afp; | package org.apache.fop.render.afp; | ||||
import java.awt.Rectangle; | import java.awt.Rectangle; | ||||
import java.awt.geom.AffineTransform; | |||||
import java.io.IOException; | import java.io.IOException; | ||||
import org.apache.xmlgraphics.image.loader.Image; | import org.apache.xmlgraphics.image.loader.Image; | ||||
setDefaultResourceLevel(graphicsObjectInfo, afpContext.getResourceManager()); | setDefaultResourceLevel(graphicsObjectInfo, afpContext.getResourceManager()); | ||||
AFPPaintingState paintingState = afpContext.getPaintingState(); | |||||
paintingState.save(); // save | |||||
AffineTransform placement = new AffineTransform(); | |||||
placement.translate(pos.x, pos.y); | |||||
paintingState.concatenate(placement); | |||||
// Image content | // Image content | ||||
ImageGraphics2D imageG2D = (ImageGraphics2D)image; | ImageGraphics2D imageG2D = (ImageGraphics2D)image; | ||||
boolean textAsShapes = false; //TODO Make configurable | boolean textAsShapes = false; //TODO Make configurable | ||||
// Create image | // Create image | ||||
afpContext.getResourceManager().createObject(graphicsObjectInfo); | afpContext.getResourceManager().createObject(graphicsObjectInfo); | ||||
paintingState.restore(); // resume | |||||
} | } | ||||
/** {@inheritDoc} */ | /** {@inheritDoc} */ |
maxPixelSize *= 3; //RGB is maximum | maxPixelSize *= 3; //RGB is maximum | ||||
} | } | ||||
} | } | ||||
float ditheringQuality = paintingState.getDitheringQuality(); | |||||
RenderedImage renderedImage = imageRendered.getRenderedImage(); | RenderedImage renderedImage = imageRendered.getRenderedImage(); | ||||
ImageInfo imageInfo = imageRendered.getInfo(); | ImageInfo imageInfo = imageRendered.getInfo(); | ||||
log.debug("Resample from " + intrinsicSize.getDimensionPx() | log.debug("Resample from " + intrinsicSize.getDimensionPx() | ||||
+ " to " + resampledDim); | + " to " + resampledDim); | ||||
} | } | ||||
renderedImage = BitmapImageUtil.convertToMonochrome(renderedImage, resampledDim); | |||||
renderedImage = BitmapImageUtil.convertToMonochrome(renderedImage, | |||||
resampledDim, ditheringQuality); | |||||
effIntrinsicSize = new ImageSize( | effIntrinsicSize = new ImageSize( | ||||
resampledDim.width, resampledDim.height, resolution); | resampledDim.width, resampledDim.height, resolution); | ||||
} else if (ditheringQuality >= 0.5f) { | |||||
renderedImage = BitmapImageUtil.convertToMonochrome(renderedImage, | |||||
intrinsicSize.getDimensionPx(), ditheringQuality); | |||||
} | } | ||||
} | } | ||||
if (cm.hasAlpha()) { | if (cm.hasAlpha()) { | ||||
pixelSize -= 8; | pixelSize -= 8; | ||||
} | } | ||||
//TODO Add support for CMYK images | |||||
byte[] imageData = null; | byte[] imageData = null; | ||||
ByteArrayOutputStream baos = new ByteArrayOutputStream(); | ByteArrayOutputStream baos = new ByteArrayOutputStream(); |
paintingState.setCMYKImagesSupported(value); | paintingState.setCMYKImagesSupported(value); | ||||
} | } | ||||
/** {@inheritDoc} */ | |||||
public void setDitheringQuality(float quality) { | |||||
this.paintingState.setDitheringQuality(quality); | |||||
} | |||||
/** {@inheritDoc} */ | /** {@inheritDoc} */ | ||||
public void setShadingMode(AFPShadingMode shadingMode) { | public void setShadingMode(AFPShadingMode shadingMode) { | ||||
this.shadingMode = shadingMode; | this.shadingMode = shadingMode; |
customizable.setBitsPerPixel(bitsPerPixel); | customizable.setBitsPerPixel(bitsPerPixel); | ||||
} | } | ||||
String dithering = imagesCfg.getAttribute("dithering-quality", "medium"); | |||||
float dq = 0.5f; | |||||
if (dithering.startsWith("min")) { | |||||
dq = 0.0f; | |||||
} else if (dithering.startsWith("max")) { | |||||
dq = 1.0f; | |||||
} else { | |||||
try { | |||||
dq = Float.parseFloat(dithering); | |||||
} catch (NumberFormatException nfe) { | |||||
//ignore and leave the default above | |||||
} | |||||
} | |||||
customizable.setDitheringQuality(dq); | |||||
// native image support | // native image support | ||||
boolean nativeImageSupport = imagesCfg.getAttributeAsBoolean("native", false); | boolean nativeImageSupport = imagesCfg.getAttributeAsBoolean("native", false); | ||||
customizable.setNativeImagesSupported(nativeImageSupport); | customizable.setNativeImagesSupported(nativeImageSupport); |
*/ | */ | ||||
public static final BufferedImage convertToMonochrome(RenderedImage img, | public static final BufferedImage convertToMonochrome(RenderedImage img, | ||||
Dimension targetDimension) { | Dimension targetDimension) { | ||||
return toBufferedImage(convertToMonochrome(img, targetDimension, 0.0f)); | |||||
} | |||||
/** | |||||
* Converts an image to a monochrome 1-bit image. Optionally, the image can be scaled. | |||||
* @param img the image to be converted | |||||
* @param targetDimension the new target dimensions or null if no scaling is necessary | |||||
* @param quality Defines the desired quality level for the conversion. | |||||
* Valid values: a value between 0.0f (fastest) and 1.0f (best) | |||||
* @return the monochrome image | |||||
*/ | |||||
public static final RenderedImage convertToMonochrome(RenderedImage img, | |||||
Dimension targetDimension, float quality) { | |||||
if (!isMonochromeImage(img)) { | |||||
if (quality >= 0.5f) { | |||||
BufferedImage bi; | |||||
Dimension orgDim = new Dimension(img.getWidth(), img.getHeight()); | |||||
if (targetDimension != null && !orgDim.equals(targetDimension)) { | |||||
//Scale only before dithering | |||||
ColorModel cm = img.getColorModel(); | |||||
BufferedImage tgt = new BufferedImage(cm, | |||||
cm.createCompatibleWritableRaster( | |||||
targetDimension.width, targetDimension.height), | |||||
cm.isAlphaPremultiplied(), null); | |||||
transferImage(img, tgt); | |||||
bi = tgt; | |||||
} else { | |||||
bi = toBufferedImage(img); | |||||
} | |||||
//Now convert to monochrome (dithering if available) | |||||
MonochromeBitmapConverter converter = createDefaultMonochromeBitmapConverter(); | |||||
if (quality >= 0.8f) { | |||||
//Activates error diffusion if JAI is available | |||||
converter.setHint("quality", Boolean.TRUE.toString()); | |||||
//Need to convert to grayscale first since otherwise, there may be encoding | |||||
//problems later with the images JAI can generate. | |||||
bi = convertToGrayscale(bi, targetDimension); | |||||
} | |||||
try { | |||||
return converter.convertToMonochrome(bi); | |||||
} catch (Exception e) { | |||||
//Provide a fallback if exotic formats are encountered | |||||
bi = convertToGrayscale(bi, targetDimension); | |||||
return converter.convertToMonochrome(bi); | |||||
} | |||||
} | |||||
} | |||||
return convertAndScaleImage(img, targetDimension, BufferedImage.TYPE_BYTE_BINARY); | return convertAndScaleImage(img, targetDimension, BufferedImage.TYPE_BYTE_BINARY); | ||||
} | } | ||||
documents. Example: the fix of marks layering will be such a case when it's done. | documents. Example: the fix of marks layering will be such a case when it's done. | ||||
--> | --> | ||||
<release version="FOP Trunk" date="TBD"> | <release version="FOP Trunk" date="TBD"> | ||||
<action context="Renderers" dev="JM" type="fix"> | |||||
AFP Output: Fixed positioning of Java2D-based images (when GOCA is enabled). | |||||
</action> | |||||
<action context="Renderers" dev="JM" type="add"> | |||||
AFP Output: Added enhanced dithering functionality for images that are converted to | |||||
bi-level images. | |||||
</action> | |||||
<action context="Renderers" dev="JM" type="fix"> | <action context="Renderers" dev="JM" type="fix"> | ||||
AFP Output: Fix for bitmap images inside an SVG or G2D graphic (printer errors) and | AFP Output: Fix for bitmap images inside an SVG or G2D graphic (printer errors) and | ||||
positioning fix for bitmaps from G2D graphics. | positioning fix for bitmaps from G2D graphics. |