diff --git a/gradle.properties b/gradle.properties index b6622fd..1dfe0cc 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,3 +6,5 @@ gradle.wrapper.version = 6.6.1 pdfbox.version = 2.0.19 zxing.version = 3.3.1 reflections.version = 0.9.11 +jfreechart.version = 1.5.1 +batik.version = 1.13 \ No newline at end of file diff --git a/graphics2d-pdfbox/build.gradle b/graphics2d-pdfbox/build.gradle new file mode 100644 index 0000000..84ef5b0 --- /dev/null +++ b/graphics2d-pdfbox/build.gradle @@ -0,0 +1,5 @@ +dependencies { + implementation "org.apache.pdfbox:pdfbox:${project.property('pdfbox.version')}" + testImplementation "org.jfree:jfreechart:${project.property('jfreechart.version')}" + testImplementation "org.apache.xmlgraphics:batik-swing:${project.property('batik.version')}" +} \ No newline at end of file diff --git a/graphics2d-pdfbox/src/main/java/module-info.java b/graphics2d-pdfbox/src/main/java/module-info.java new file mode 100644 index 0000000..cc83995 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/module-info.java @@ -0,0 +1,6 @@ +module org.xbib.graphics.graphics2d.pdfbox { + exports org.xbib.graphics.graphics2d.pdfbox; + requires transitive org.apache.pdfbox; + requires transitive java.desktop; + requires java.logging; +} \ No newline at end of file diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/CMYKColor.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/CMYKColor.java new file mode 100644 index 0000000..e10c76f --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/CMYKColor.java @@ -0,0 +1,79 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; + +import java.awt.Color; +import java.io.IOException; + +/** + * This color class represents a CMYK Color. You can use this class if you want + * to paint with DeviceCMYK Colors + */ +@SuppressWarnings("serial") +public class CMYKColor extends Color { + private final float c, m, y, k; + private final PDColorSpace colorSpace; + + public CMYKColor(int c, int m, int y, int k, int alpha) { + this(c / 255f, m / 255f, y / 255f, k / 255f, alpha); + } + + public CMYKColor(int c, int m, int y, int k) { + this(c / 255f, m / 255f, y / 255f, k / 255f); + } + + public CMYKColor(float c, float m, float y, float k) { + this(c, m, y, k, 255); + } + + private static int toRGBValue(float c, float m, float y, float k, int alpha, PDColorSpace colorSpace) { + float[] rgb; + try { + rgb = colorSpace.toRGB(new float[]{c, m, y, k}); + int r = ((int) (rgb[0] * 0xFF)) & 0xFF; + int g = ((int) (rgb[1] * 0xFF)) & 0xFF; + int b = ((int) (rgb[2] * 0xFF)) & 0xFF; + return alpha << 24 | r << 16 | g << 8 | b; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public CMYKColor(float c, float m, float y, float k, int alpha) { + this(c, m, y, k, alpha, PDDeviceCMYK.INSTANCE); + } + + public CMYKColor(float c, float m, float y, float k, int alpha, PDColorSpace colorSpace) { + super(toRGBValue(c, m, y, k, alpha, colorSpace), true); + this.c = c; + this.m = m; + this.y = y; + this.k = k; + this.colorSpace = colorSpace; + } + + public float getC() { + return c; + } + + public float getM() { + return m; + } + + public float getY() { + return y; + } + + public float getK() { + return k; + } + + /** + * @return the PDColor represented by this color object + */ + public PDColor toPDColor() { + return new PDColor(new float[]{getC(), getM(), getY(), getK()}, colorSpace); + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/ColorMapper.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/ColorMapper.java new file mode 100644 index 0000000..2d32a60 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/ColorMapper.java @@ -0,0 +1,20 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; + +import java.awt.Color; + +/** + * Map Color to PDColor + */ +public interface ColorMapper { + /** + * Map the given Color to a PDColor + * + * @param contentStream the content stream + * @param color the color to map + * @return the mapped color + */ + PDColor mapColor(PDPageContentStream contentStream, Color color); +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultColorMapper.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultColorMapper.java new file mode 100644 index 0000000..a3c975c --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultColorMapper.java @@ -0,0 +1,31 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceCMYK; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; + +import java.awt.Color; + +public class DefaultColorMapper implements ColorMapper { + + @Override + public PDColor mapColor(PDPageContentStream contentStream, Color color) { + if (color == null) { + return new PDColor(new float[]{1f, 1f, 1f}, PDDeviceRGB.INSTANCE); + } + if (color.getClass().getSimpleName().equals("CMYKColor")) { + float c = DefaultPaintApplier.getPropertyValue(color, "getC"); + float m = DefaultPaintApplier.getPropertyValue(color, "getM"); + float y = DefaultPaintApplier.getPropertyValue(color, "getY"); + float k = DefaultPaintApplier.getPropertyValue(color, "getK"); + return new PDColor(new float[]{c, m, y, k}, PDDeviceCMYK.INSTANCE); + } + // Our own CMYK Color class + if (color instanceof CMYKColor) { + return ((CMYKColor) color).toPDColor(); + } + float[] components = new float[]{color.getRed() / 255f, color.getGreen() / 255f, color.getBlue() / 255f}; + return new PDColor(components, PDDeviceRGB.INSTANCE); + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultDrawControl.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultDrawControl.java new file mode 100644 index 0000000..5460f08 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultDrawControl.java @@ -0,0 +1,33 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import java.awt.Shape; + +/** + * Default implementation which does nothing. You can derive from it to only + * override the needed methods + */ +public class DefaultDrawControl implements DrawControl { + + public static final DefaultDrawControl INSTANCE = new DefaultDrawControl(); + + protected DefaultDrawControl() { + } + + @Override + public Shape transformShapeBeforeFill(Shape shape, IDrawControlEnv env) { + return shape; + } + + @Override + public Shape transformShapeBeforeDraw(Shape shape, IDrawControlEnv env) { + return shape; + } + + @Override + public void afterShapeFill(Shape shape, IDrawControlEnv env) { + } + + @Override + public void afterShapeDraw(Shape shape, IDrawControlEnv env) { + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextDrawer.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextDrawer.java new file mode 100644 index 0000000..838628c --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextDrawer.java @@ -0,0 +1,601 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.fontbox.ttf.TrueTypeCollection; +import org.apache.fontbox.ttf.TrueTypeFont; +import org.apache.pdfbox.io.IOUtils; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.util.Matrix; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Graphics; +import java.awt.Paint; +import java.awt.font.FontRenderContext; +import java.awt.font.LineMetrics; +import java.awt.font.TextAttribute; +import java.awt.geom.Rectangle2D; +import java.io.Closeable; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.text.AttributedCharacterIterator; +import java.text.CharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default implementation to draw fonts. You can reuse instances of this class + * within a PDDocument for more then one {@link PdfBoxGraphics2D}. + *

+ * Just ensure that you call close after you closed the PDDocument to free any + * temporary files. + */ +public class DefaultFontTextDrawer implements FontTextDrawer, Closeable { + + private static final Logger logger = Logger.getLogger(DefaultFontTextDrawer.class.getName()); + + @Override + public void close() { + for (File tempFile : tempFiles) { + tempFile.delete(); + } + tempFiles.clear(); + fontFiles.clear(); + fontMap.clear(); + } + + private static class FontEntry { + String overrideName; + File file; + } + + private final List fontFiles = new ArrayList(); + private final List tempFiles = new ArrayList(); + private final Map fontMap = new HashMap(); + + /** + * Register a font. If possible, try to use a font file, i.e. + * {@link #registerFont(String, File)}. This method will lead to the creation of + * a temporary file which stores the font data. + * + * @param fontName the name of the font to use. If null, the name is taken from the + * font. + * @param fontStream the input stream of the font. This file must be a ttf/otf file! + * You have to close the stream outside, this method will not close + * the stream. + * @throws IOException when something goes wrong with reading the font or writing the + * font to the content stream of the PDF: + */ + public void registerFont(String fontName, InputStream fontStream) throws IOException { + File fontFile = File.createTempFile("pdfboxgfx2dfont", ".ttf"); + try (FileOutputStream out = new FileOutputStream(fontFile)) { + IOUtils.copy(fontStream, out); + } + fontFile.deleteOnExit(); + tempFiles.add(fontFile); + registerFont(fontName, fontFile); + } + + /** + * Register a font. + * + * @param fontName the name of the font to use. If null, the name is taken from the + * font. + * @param fontFile the font file. This file must exist for the live time of this + * object, as the font data will be read lazy on demand + */ + @SuppressWarnings("WeakerAccess") + public void registerFont(String fontName, File fontFile) { + if (!fontFile.exists()) + throw new IllegalArgumentException("Font " + fontFile + " does not exist!"); + FontEntry entry = new FontEntry(); + entry.overrideName = fontName; + entry.file = fontFile; + fontFiles.add(entry); + } + + /** + * Override for registerFont(null,fontFile) + * + * @param fontFile the font file + */ + @SuppressWarnings("WeakerAccess") + public void registerFont(File fontFile) { + registerFont(null, fontFile); + } + + /** + * Override for registerFont(null,fontStream) + * + * @param fontStream the font file + * @throws IOException when something goes wrong with reading the font or writing the + * font to the content stream of the PDF: + */ + @SuppressWarnings("WeakerAccess") + public void registerFont(InputStream fontStream) throws IOException { + registerFont(null, fontStream); + } + + /** + * Register a font which is already associated with the PDDocument + * + * @param name the name of the font as returned by + * {@link Font#getFontName()}. This name is used for the + * mapping the java.awt.Font to this PDFont. + * @param font the PDFont to use. This font must be loaded in the current + * document. + */ + @SuppressWarnings("WeakerAccess") + public void registerFont(String name, PDFont font) { + fontMap.put(name, font); + } + + /** + * @return true if the font mapping is populated on demand. This is usually only + * the case if this class has been derived. The default implementation + * just checks for this. + */ + @SuppressWarnings("WeakerAccess") + protected boolean hasDynamicFontMapping() { + return getClass() != DefaultFontTextDrawer.class; + } + + @Override + public boolean canDrawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) + throws IOException, FontFormatException { + /* + * When no font is registered we can not display the text using a font... + */ + if (fontMap.size() == 0 && fontFiles.size() == 0 && !hasDynamicFontMapping()) + return false; + + boolean run = true; + StringBuilder sb = new StringBuilder(); + while (run) { + + Font attributeFont = (Font) iterator.getAttribute(TextAttribute.FONT); + if (attributeFont == null) + attributeFont = env.getFont(); + if (mapFont(attributeFont, env) == null) + return false; + + /* + * We can not do a Background on the text currently. + */ + if (iterator.getAttribute(TextAttribute.BACKGROUND) != null) + return false; + + boolean isStrikeThrough = TextAttribute.STRIKETHROUGH_ON + .equals(iterator.getAttribute(TextAttribute.STRIKETHROUGH)); + boolean isUnderline = TextAttribute.UNDERLINE_ON + .equals(iterator.getAttribute(TextAttribute.UNDERLINE)); + boolean isLigatures = TextAttribute.LIGATURES_ON + .equals(iterator.getAttribute(TextAttribute.LIGATURES)); + if (isStrikeThrough || isUnderline || isLigatures) + return false; + + run = iterateRun(iterator, sb); + String s = sb.toString(); + int l = s.length(); + for (int i = 0; i < l; ) { + int codePoint = s.codePointAt(i); + switch (Character.getDirectionality(codePoint)) { + /* + * We can handle normal LTR. + */ + case Character.DIRECTIONALITY_LEFT_TO_RIGHT: + case Character.DIRECTIONALITY_EUROPEAN_NUMBER: + case Character.DIRECTIONALITY_EUROPEAN_NUMBER_SEPARATOR: + case Character.DIRECTIONALITY_EUROPEAN_NUMBER_TERMINATOR: + case Character.DIRECTIONALITY_WHITESPACE: + case Character.DIRECTIONALITY_COMMON_NUMBER_SEPARATOR: + case Character.DIRECTIONALITY_NONSPACING_MARK: + case Character.DIRECTIONALITY_BOUNDARY_NEUTRAL: + case Character.DIRECTIONALITY_PARAGRAPH_SEPARATOR: + case Character.DIRECTIONALITY_SEGMENT_SEPARATOR: + case Character.DIRECTIONALITY_OTHER_NEUTRALS: + case Character.DIRECTIONALITY_ARABIC_NUMBER: + break; + case Character.DIRECTIONALITY_RIGHT_TO_LEFT: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_EMBEDDING: + case Character.DIRECTIONALITY_RIGHT_TO_LEFT_OVERRIDE: + case Character.DIRECTIONALITY_POP_DIRECTIONAL_FORMAT: + /* + * We can not handle this + */ + return false; + default: + /* + * Default: We can not handle this + */ + return false; + } + + if (!attributeFont.canDisplay(codePoint)) + return false; + + i += Character.charCount(codePoint); + } + } + return true; + } + + @Override + public void drawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) + throws IOException, FontFormatException { + PDPageContentStream contentStream = env.getContentStream(); + + contentStream.beginText(); + + Matrix textMatrix = new Matrix(); + textMatrix.scale(1, -1); + contentStream.setTextMatrix(textMatrix); + + StringBuilder sb = new StringBuilder(); + boolean run = true; + while (run) { + + Font attributeFont = (Font) iterator.getAttribute(TextAttribute.FONT); + if (attributeFont == null) + attributeFont = env.getFont(); + + Number fontSize = ((Number) iterator.getAttribute(TextAttribute.SIZE)); + if (fontSize != null) + attributeFont = attributeFont.deriveFont(fontSize.floatValue()); + PDFont font = applyFont(attributeFont, env); + + Paint paint = (Paint) iterator.getAttribute(TextAttribute.FOREGROUND); + if (paint == null) + paint = env.getPaint(); + + boolean isStrikeThrough = TextAttribute.STRIKETHROUGH_ON + .equals(iterator.getAttribute(TextAttribute.STRIKETHROUGH)); + boolean isUnderline = TextAttribute.UNDERLINE_ON + .equals(iterator.getAttribute(TextAttribute.UNDERLINE)); + boolean isLigatures = TextAttribute.LIGATURES_ON + .equals(iterator.getAttribute(TextAttribute.LIGATURES)); + + run = iterateRun(iterator, sb); + String text = sb.toString(); + + /* + * Apply the paint + */ + env.applyPaint(paint, null); + + /* + * If we force the text write we may encounter situations where the font can not + * display the characters. PDFBox will throw an exception in this case. We will + * just silently ignore the text and not display it instead. + */ + try { + showTextOnStream(env, contentStream, attributeFont, font, isStrikeThrough, + isUnderline, isLigatures, text); + } catch (IllegalArgumentException e) { + if (font instanceof PDType1Font && !font.isEmbedded()) { + /* + * We tried to use a builtin default font, but it does not have the needed + * characters. So we use a embedded font as fallback. + */ + try { + if (fallbackFontUnknownEncodings == null) + fallbackFontUnknownEncodings = findFallbackFont(env); + if (fallbackFontUnknownEncodings != null) { + env.getContentStream().setFont(fallbackFontUnknownEncodings, + attributeFont.getSize2D()); + showTextOnStream(env, contentStream, attributeFont, + fallbackFontUnknownEncodings, isStrikeThrough, isUnderline, + isLigatures, text); + e = null; + } + } catch (IllegalArgumentException e1) { + e = e1; + } + } + + if (e != null) + logger.log(Level.WARNING, "PDFBoxGraphics: Can not map text " + text + " with font " + + attributeFont.getFontName() + ": " + e.getMessage()); + } + } + contentStream.endText(); + } + + @Override + public FontMetrics getFontMetrics(final Font f, IFontTextDrawerEnv env) + throws IOException, FontFormatException { + final FontMetrics defaultMetrics = env.getCalculationGraphics().getFontMetrics(f); + final PDFont pdFont = mapFont(f, env); + /* + * By default we delegate to the buffered image based calculation. This is wrong + * as soon as we use the native PDF Box font, as those have sometimes different widths. + * + * But it is correct and fine as long as we use vector shapes. + */ + if (pdFont == null) + return defaultMetrics; + return new FontMetrics(f) { + public int getDescent() { + return defaultMetrics.getDescent(); + } + + public int getHeight() { + return defaultMetrics.getHeight(); + } + + public int getMaxAscent() { + return defaultMetrics.getMaxAscent(); + } + + public int getMaxDescent() { + return defaultMetrics.getMaxDescent(); + } + + public boolean hasUniformLineMetrics() { + return defaultMetrics.hasUniformLineMetrics(); + } + + public LineMetrics getLineMetrics(String str, Graphics context) { + return defaultMetrics.getLineMetrics(str, context); + } + + public LineMetrics getLineMetrics(String str, int beginIndex, int limit, + Graphics context) { + return defaultMetrics.getLineMetrics(str, beginIndex, limit, context); + } + + public LineMetrics getLineMetrics(char[] chars, int beginIndex, int limit, + Graphics context) { + return defaultMetrics.getLineMetrics(chars, beginIndex, limit, context); + } + + public LineMetrics getLineMetrics(CharacterIterator ci, int beginIndex, int limit, + Graphics context) { + return defaultMetrics.getLineMetrics(ci, beginIndex, limit, context); + } + + public Rectangle2D getStringBounds(String str, Graphics context) { + return defaultMetrics.getStringBounds(str, context); + } + + public Rectangle2D getStringBounds(String str, int beginIndex, int limit, + Graphics context) { + return defaultMetrics.getStringBounds(str, beginIndex, limit, context); + } + + public Rectangle2D getStringBounds(char[] chars, int beginIndex, int limit, + Graphics context) { + return defaultMetrics.getStringBounds(chars, beginIndex, limit, context); + } + + public Rectangle2D getStringBounds(CharacterIterator ci, int beginIndex, int limit, + Graphics context) { + return defaultMetrics.getStringBounds(ci, beginIndex, limit, context); + } + + public Rectangle2D getMaxCharBounds(Graphics context) { + return defaultMetrics.getMaxCharBounds(context); + } + + @Override + public int getAscent() { + return defaultMetrics.getAscent(); + } + + @Override + public int getMaxAdvance() { + return defaultMetrics.getMaxAdvance(); + } + + @Override + public int getLeading() { + return defaultMetrics.getLeading(); + } + + @Override + public FontRenderContext getFontRenderContext() { + return defaultMetrics.getFontRenderContext(); + } + + @Override + public int charWidth(char ch) { + char[] chars = {ch}; + return charsWidth(chars, 0, chars.length); + } + + @Override + public int charWidth(int codePoint) { + char[] data = Character.toChars(codePoint); + return charsWidth(data, 0, data.length); + } + + @Override + public int charsWidth(char[] data, int off, int len) { + return stringWidth(new String(data, off, len)); + } + + @Override + public int stringWidth(String str) { + try { + return (int) (pdFont.getStringWidth(str) / 1000 * f.getSize()); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (IllegalArgumentException e) { + /* + * We let unknown chars be handled with + */ + return defaultMetrics.stringWidth(str); + } + } + + @Override + public int[] getWidths() { + try { + int[] first256Widths = new int[256]; + for (int i = 0; i < first256Widths.length; i++) + first256Widths[i] = (int) (pdFont.getWidth(i) / 1000 * f.getSize()); + return first256Widths; + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + }; + } + + private PDFont fallbackFontUnknownEncodings; + + private PDFont findFallbackFont(IFontTextDrawerEnv env) throws IOException { + /* + * We search for the right font in the folders... We try to use + * LucidaSansRegular and if not found Arial, because this fonts often exists. We + * use the Java default font as fallback. + * + * Normally this method is only used and called if a default font misses some + * special characters, e.g. Hebrew or Arabic characters. + */ + String javaHome = System.getProperty("java.home", "."); + String javaFontDir = javaHome + "/lib/fonts"; + String windir = System.getenv("WINDIR"); + if (windir == null) + windir = javaFontDir; + File[] paths = new File[]{new File(new File(windir), "fonts"), + new File(System.getProperty("user.dir", ".")), + // Mac Fonts + new File("/Library/Fonts"), new File("/System/Library/Fonts/Supplemental/"), + // Unix Fonts + new File("/usr/share/fonts/truetype"), new File("/usr/share/fonts/truetype/dejavu"), + new File("/usr/share/fonts/truetype/liberation"), + new File("/usr/share/fonts/truetype/noto"), new File(javaFontDir)}; + for (String fontFileName : new String[]{"LucidaSansRegular.ttf", "arial.ttf", "Arial.ttf", + "DejaVuSans.ttf", "LiberationMono-Regular.ttf", "NotoSerif-Regular.ttf", + "Arial Unicode.ttf", "Tahoma.ttf"}) { + for (File path : paths) { + File arialFile = new File(path, fontFileName); + if (arialFile.exists()) { + // We try to use the first font we can find and use. + PDType0Font pdType0Font = tryToLoadFont(env, arialFile); + if (pdType0Font != null) + return pdType0Font; + } + } + } + return null; + } + + private PDType0Font tryToLoadFont(IFontTextDrawerEnv env, File foundFontFile) throws IOException { + try { + return PDType0Font.load(env.getDocument(), foundFontFile); + } catch (IOException e) { + // The font may be have a embed restriction. + return null; + } + } + + private void showTextOnStream(IFontTextDrawerEnv env, PDPageContentStream contentStream, + Font attributeFont, PDFont font, boolean isStrikeThrough, boolean isUnderline, + boolean isLigatures, String text) throws IOException { + if (isStrikeThrough || isUnderline) { + // noinspection unused + float stringWidth = font.getStringWidth(text); + // noinspection unused + LineMetrics lineMetrics = attributeFont + .getLineMetrics(text, env.getFontRenderContext()); + /* + * TODO: We can not draw that yet, we must do that later. While in textmode its + * not possible to draw lines... + */ + } + // noinspection StatementWithEmptyBody + if (isLigatures) { + /* + * No idea how to map this ... + */ + } + contentStream.showText(text); + } + + private PDFont applyFont(Font font, IFontTextDrawerEnv env) + throws IOException, FontFormatException { + PDFont fontToUse = mapFont(font, env); + if (fontToUse == null) { + /* + * If we have no font but are forced to apply a font, we just use the default + * builtin PDF font... + */ + fontToUse = DefaultFontTextDrawerDefaultFonts.chooseMatchingHelvetica(font); + } + env.getContentStream().setFont(fontToUse, font.getSize2D()); + return fontToUse; + } + + /** + * Try to map the java.awt.Font to a PDFont. + * + * @param font the java.awt.Font for which a mapping should be found + * @param env environment of the font mapper + * @return the PDFont or null if none can be found. + * @throws IOException when the font can not be loaded + * @throws FontFormatException when the font file can not be loaded + */ + @SuppressWarnings("WeakerAccess") + protected PDFont mapFont(final Font font, final IFontTextDrawerEnv env) + throws IOException, FontFormatException { + /* + * If we have any font registering's, we must perform them now + */ + for (final FontEntry fontEntry : fontFiles) { + if (fontEntry.overrideName == null) { + Font javaFont = Font.createFont(Font.TRUETYPE_FONT, fontEntry.file); + fontEntry.overrideName = javaFont.getFontName(); + } + if (fontEntry.file.getName().toLowerCase(Locale.US).endsWith(".ttc")) { + TrueTypeCollection collection = new TrueTypeCollection(fontEntry.file); + collection.processAllFonts(new TrueTypeCollection.TrueTypeFontProcessor() { + @Override + public void process(TrueTypeFont ttf) throws IOException { + PDFont pdFont = PDType0Font.load(env.getDocument(), ttf, true); + fontMap.put(fontEntry.overrideName, pdFont); + fontMap.put(pdFont.getName(), pdFont); + } + }); + } else { + /* + * We load the font using the file. + */ + PDFont pdFont = PDType0Font.load(env.getDocument(), fontEntry.file); + fontMap.put(fontEntry.overrideName, pdFont); + } + } + fontFiles.clear(); + + return fontMap.get(font.getFontName()); + } + + private boolean iterateRun(AttributedCharacterIterator iterator, StringBuilder sb) { + sb.setLength(0); + int charCount = iterator.getRunLimit() - iterator.getRunStart(); + while (charCount-- >= 0) { + char c = iterator.current(); + iterator.next(); + if (c == AttributedCharacterIterator.DONE) { + return false; + } else { + sb.append(c); + } + } + return true; + } + +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextDrawerDefaultFonts.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextDrawerDefaultFonts.java new file mode 100644 index 0000000..a3cc07b --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextDrawerDefaultFonts.java @@ -0,0 +1,116 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType1Font; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.io.IOException; + +/** + * Like {@link DefaultFontTextDrawer}, but tries to use default fonts + * whenever possible. Default fonts are not embedded. You can register + * additional font files. If no font mapping is found, Helvetica is used. + * This will fallback to vectorized text if any kind of RTL text is rendered + * and/or any other not supported feature is used. + */ +public class DefaultFontTextDrawerDefaultFonts extends DefaultFontTextDrawer { + @Override + protected PDFont mapFont(Font font, IFontTextDrawerEnv env) throws IOException, FontFormatException { + PDFont pdFont = mapDefaultFonts(font); + if (pdFont != null) + return pdFont; + + /* + * Do we have a manual registered mapping with a font file? + */ + pdFont = super.mapFont(font, env); + if (pdFont != null) + return pdFont; + return chooseMatchingHelvetica(font); + } + + /** + * Find a PDFont for the given font object, which does not need to be embedded. + * + * @param font font for which to find a suitable default font + * @return null if no default font is found or a default font which does not + * need to be embedded. + */ + public static PDFont mapDefaultFonts(Font font) { + /* + * Map default font names to the matching families. + */ + if (fontNameEqualsAnyOf(font, Font.SANS_SERIF, Font.DIALOG, Font.DIALOG_INPUT, "Arial", "Helvetica")) + return chooseMatchingHelvetica(font); + if (fontNameEqualsAnyOf(font, Font.MONOSPACED, "courier", "courier new")) + return chooseMatchingCourier(font); + if (fontNameEqualsAnyOf(font, Font.SERIF, "Times", "Times New Roman", "Times Roman")) + return chooseMatchingTimes(font); + if (fontNameEqualsAnyOf(font, "Symbol")) + return PDType1Font.SYMBOL; + if (fontNameEqualsAnyOf(font, "ZapfDingbats", "Dingbats")) + return PDType1Font.ZAPF_DINGBATS; + return null; + } + + private static boolean fontNameEqualsAnyOf(Font font, String... names) { + String name = font.getName(); + for (String fontName : names) { + if (fontName.equalsIgnoreCase(name)) + return true; + } + return false; + } + + /** + * Get a PDType1Font.TIMES-variant, which matches the given font + * + * @param font Font to get the styles from + * @return a PDFont Times variant which matches the style in the given Font + * object. + */ + public static PDFont chooseMatchingTimes(Font font) { + if ((font.getStyle() & (Font.ITALIC | Font.BOLD)) == (Font.ITALIC | Font.BOLD)) + return PDType1Font.TIMES_BOLD_ITALIC; + if ((font.getStyle() & Font.ITALIC) == Font.ITALIC) + return PDType1Font.TIMES_ITALIC; + if ((font.getStyle() & Font.BOLD) == Font.BOLD) + return PDType1Font.TIMES_BOLD; + return PDType1Font.TIMES_ROMAN; + } + + /** + * Get a PDType1Font.COURIER-variant, which matches the given font + * + * @param font Font to get the styles from + * @return a PDFont Courier variant which matches the style in the given Font + * object. + */ + public static PDFont chooseMatchingCourier(Font font) { + if ((font.getStyle() & (Font.ITALIC | Font.BOLD)) == (Font.ITALIC | Font.BOLD)) + return PDType1Font.COURIER_BOLD_OBLIQUE; + if ((font.getStyle() & Font.ITALIC) == Font.ITALIC) + return PDType1Font.COURIER_OBLIQUE; + if ((font.getStyle() & Font.BOLD) == Font.BOLD) + return PDType1Font.COURIER_BOLD; + return PDType1Font.COURIER; + } + + /** + * Get a PDType1Font.HELVETICA-variant, which matches the given font + * + * @param font Font to get the styles from + * @return a PDFont Helvetica variant which matches the style in the given Font + * object. + */ + public static PDFont chooseMatchingHelvetica(Font font) { + if ((font.getStyle() & (Font.ITALIC | Font.BOLD)) == (Font.ITALIC | Font.BOLD)) + return PDType1Font.HELVETICA_BOLD_OBLIQUE; + if ((font.getStyle() & Font.ITALIC) == Font.ITALIC) + return PDType1Font.HELVETICA_OBLIQUE; + if ((font.getStyle() & Font.BOLD) == Font.BOLD) + return PDType1Font.HELVETICA_BOLD; + return PDType1Font.HELVETICA; + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextForcedDrawer.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextForcedDrawer.java new file mode 100644 index 0000000..fcfcadc --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultFontTextForcedDrawer.java @@ -0,0 +1,13 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import java.text.AttributedCharacterIterator; + +/** + * Always draw using text, even if we know that we can not map the text correct + */ +public class DefaultFontTextForcedDrawer extends DefaultFontTextDrawerDefaultFonts { + @Override + public boolean canDrawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) { + return true; + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultPaintApplier.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultPaintApplier.java new file mode 100644 index 0000000..fb2597a --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DefaultPaintApplier.java @@ -0,0 +1,864 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.cos.COSArray; +import org.apache.pdfbox.cos.COSBase; +import org.apache.pdfbox.cos.COSBoolean; +import org.apache.pdfbox.cos.COSDictionary; +import org.apache.pdfbox.cos.COSFloat; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.multipdf.PDFCloneUtility; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.COSObjectable; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.common.function.PDFunctionType3; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDPattern; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; +import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; +import org.apache.pdfbox.pdmodel.graphics.shading.PDShadingType3; +import org.apache.pdfbox.pdmodel.graphics.shading.ShadingPaint; +import org.apache.pdfbox.pdmodel.graphics.state.PDExtendedGraphicsState; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.apache.pdfbox.util.Matrix; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Composite; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.MultipleGradientPaint; +import java.awt.Paint; +import java.awt.TexturePaint; +import java.awt.geom.AffineTransform; +import java.awt.geom.Point2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Default paint mapper. + */ +public class DefaultPaintApplier implements PaintApplier { + + private static final Logger logger = Logger.getLogger(DefaultPaintApplier.class.getName()); + + @SuppressWarnings("WeakerAccess") + protected static class PaintApplierState { + protected PDDocument document; + protected PDPageContentStream contentStream; + @SuppressWarnings("WeakerAccess") + protected ColorMapper colorMapper; + @SuppressWarnings("WeakerAccess") + protected ImageEncoder imageEncoder; + @SuppressWarnings("WeakerAccess") + protected PDResources resources; + @SuppressWarnings("WeakerAccess") + protected PDExtendedGraphicsState pdExtendedGraphicsState; + @SuppressWarnings("WeakerAccess") + protected Composite composite; + private COSDictionary dictExtendedState; + private IPaintEnv env; + public AffineTransform tf; + /** + * This transform is only set, when we apply a nested + * paint (e.g. a TilingPattern's paint) + */ + protected AffineTransform nestedTransform; + + private void ensureExtendedState() { + if (pdExtendedGraphicsState == null) { + this.dictExtendedState = new COSDictionary(); + this.dictExtendedState.setItem(COSName.TYPE, COSName.EXT_G_STATE); + pdExtendedGraphicsState = new PDExtendedGraphicsState(this.dictExtendedState); + } + } + } + + private final ExtGStateCache extGStateCache = new ExtGStateCache(); + private final PDShadingCache shadingCache = new PDShadingCache(); + + @Override + public PDShading applyPaint(Paint paint, PDPageContentStream contentStream, AffineTransform tf, + IPaintEnv env) throws IOException { + PaintApplierState state = new PaintApplierState(); + state.document = env.getDocument(); + state.resources = env.getResources(); + state.contentStream = contentStream; + state.colorMapper = env.getColorMapper(); + state.imageEncoder = env.getImageEncoder(); + state.composite = env.getComposite(); + state.pdExtendedGraphicsState = null; + state.env = env; + state.tf = tf; + state.nestedTransform = null; + PDShading shading = applyPaint(paint, state); + if (state.pdExtendedGraphicsState != null) + contentStream.setGraphicsStateParameters( + extGStateCache.makeUnqiue(state.pdExtendedGraphicsState)); + return shading; + } + + @SuppressWarnings("WeakerAccess") + protected void applyAsStrokingColor(Color color, PaintApplierState state) throws IOException { + PDPageContentStream contentStream = state.contentStream; + ColorMapper colorMapper = state.colorMapper; + contentStream.setStrokingColor(colorMapper.mapColor(contentStream, color)); + contentStream.setNonStrokingColor(colorMapper.mapColor(contentStream, color)); + + int alpha = color.getAlpha(); + if (alpha < 255) { + state.ensureExtendedState(); + Float strokingAlphaConstant = state.pdExtendedGraphicsState.getStrokingAlphaConstant(); + if (strokingAlphaConstant == null) + strokingAlphaConstant = 1f; + state.pdExtendedGraphicsState + .setStrokingAlphaConstant(strokingAlphaConstant * (alpha / 255f)); + Float nonStrokingAlphaConstant = state.pdExtendedGraphicsState + .getNonStrokingAlphaConstant(); + if (nonStrokingAlphaConstant == null) + nonStrokingAlphaConstant = 1f; + state.pdExtendedGraphicsState + .setNonStrokingAlphaConstant(nonStrokingAlphaConstant * (alpha / 255f)); + } + } + + private PDShading applyPaint(Paint paint, PaintApplierState state) throws IOException { + applyComposite(state); + if (paint == null) + return null; + + String simpleName = paint.getClass().getSimpleName(); + if (paint instanceof Color) { + applyAsStrokingColor((Color) paint, state); + } else if (simpleName.equals("LinearGradientPaint")) { + return shadingCache.makeUnqiue(buildLinearGradientShading(paint, state)); + } else if (simpleName.equals("RadialGradientPaint")) { + return shadingCache.makeUnqiue(buildRadialGradientShading(paint, state)); + } else if (simpleName.equals("PatternPaint")) { + applyPatternPaint(paint, state); + } else if (simpleName.equals("TilingPaint")) { + //applyPdfBoxTilingPaint(paint, state); + } else if (paint instanceof GradientPaint) { + return shadingCache.makeUnqiue(buildGradientShading((GradientPaint) paint, state)); + } else if (paint instanceof TexturePaint) { + applyTexturePaint((TexturePaint) paint, state); + } else if (paint instanceof ShadingPaint) { + // PDFBox paint, we can import the shading directly + return shadingCache + .makeUnqiue(importPDFBoxShadingPaint((ShadingPaint) paint, state)); + } else { + logger.log(Level.WARNING, "Don't know paint " + paint.getClass().getName()); + } + + return null; + } + + private PDShading importPDFBoxShadingPaint(ShadingPaint paint, PaintApplierState state) + throws IOException { + PDFCloneUtility pdfCloneUtility = new PDFCloneUtility(state.document); + + Matrix matrix = paint.getMatrix(); + PDShading shading = paint.getShading(); + + state.contentStream.transform(matrix); + return PDShading.create((COSDictionary) pdfCloneUtility + .cloneForNewDocument(shading.getCOSObject())); + } + + private void applyPatternPaint(Paint paint, PaintApplierState state) throws IOException { + Rectangle2D anchorRect = getPropertyValue(paint, "getPatternRect"); + AffineTransform paintPatternTransform = getPropertyValue(paint, "getPatternTransform"); + PDTilingPattern pattern = new PDTilingPattern(); + pattern.setPaintType(PDTilingPattern.PAINT_COLORED); + pattern.setTilingType(PDTilingPattern.TILING_CONSTANT_SPACING_FASTER_TILING); + + pattern.setBBox(new PDRectangle((float) anchorRect.getX(), (float) anchorRect.getY(), + (float) anchorRect.getWidth(), (float) anchorRect.getHeight())); + pattern.setXStep((float) anchorRect.getWidth()); + pattern.setYStep((float) anchorRect.getHeight()); + + AffineTransform patternTransform = new AffineTransform(); + if (paintPatternTransform != null) { + paintPatternTransform = new AffineTransform(paintPatternTransform); + paintPatternTransform.preConcatenate(state.tf); + patternTransform.concatenate(paintPatternTransform); + } else + patternTransform.concatenate(state.tf); + patternTransform.scale(1f, -1f); + pattern.setMatrix(patternTransform); + + PDAppearanceStream appearance = new PDAppearanceStream(state.document); + appearance.setResources(pattern.getResources()); + appearance.setBBox(pattern.getBBox()); + + Object graphicsNode = getPropertyValue(paint, "getGraphicsNode"); + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(state.document, pattern.getBBox(), + state.env.getGraphics2D()); + try { + Method paintMethod = graphicsNode.getClass().getMethod("paint", Graphics2D.class); + paintMethod.invoke(graphicsNode, pdfBoxGraphics2D); + } catch (Exception e) { + logger.log(Level.WARNING, "PaintApplier error while drawing Batik PatternPaint " + e.getMessage()); + return; + } + pdfBoxGraphics2D.dispose(); + PDFormXObject xFormObject = pdfBoxGraphics2D.getXFormObject(); + + PDPageContentStream imageContentStream = new PDPageContentStream(state.document, appearance, + ((COSStream) pattern.getCOSObject()).createOutputStream()); + imageContentStream.drawForm(xFormObject); + imageContentStream.close(); + + PDColorSpace patternCS1 = new PDPattern(null); + COSName tilingPatternName = state.resources.add(pattern); + PDColor patternColor = new PDColor(tilingPatternName, patternCS1); + + state.contentStream.setNonStrokingColor(patternColor); + state.contentStream.setStrokingColor(patternColor); + } + + + /*private void applyPdfBoxTilingPaint(Paint paint, PaintApplierState state) { + Paint tilingPaint = PrivateFieldAccessor.getPrivateField(paint, "paint"); + Matrix patternMatrix = PrivateFieldAccessor.getPrivateField(paint, "patternMatrix"); + state.nestedTransform = patternMatrix.createAffineTransform(); + applyPaint(tilingPaint, state); + }*/ + + private void applyComposite(PaintApplierState state) { + if (state.composite == null) + return; + + // Possibly set the alpha constant + float alpha = 1; + COSName blendMode = COSName.COMPATIBLE; + int rule = AlphaComposite.SRC; + + if (state.composite instanceof AlphaComposite) { + AlphaComposite composite = (AlphaComposite) state.composite; + alpha = composite.getAlpha(); + rule = composite.getRule(); + } else if (state.composite.getClass().getSimpleName().equals("SVGComposite")) { + alpha = getPropertyValue(state.composite, "alpha"); + rule = getPropertyValue(state.composite, "rule"); + } else { + logger.log(Level.WARNING, "Unknown composite " + state.composite.getClass().getSimpleName()); + } + + state.ensureExtendedState(); + if (alpha < 1) { + assert state.pdExtendedGraphicsState != null; + state.pdExtendedGraphicsState.setStrokingAlphaConstant(alpha); + state.pdExtendedGraphicsState.setNonStrokingAlphaConstant(alpha); + } + /* + * Try to map the alpha rule into blend modes + */ + switch (rule) { + case AlphaComposite.CLEAR: + break; + case AlphaComposite.SRC: + blendMode = COSName.NORMAL; + break; + case AlphaComposite.SRC_OVER: + blendMode = COSName.COMPATIBLE; + break; + case AlphaComposite.XOR: + blendMode = COSName.EXCLUSION; + break; + case AlphaComposite.DST: + break; + case AlphaComposite.DST_ATOP: + break; + case AlphaComposite.SRC_ATOP: + blendMode = COSName.COMPATIBLE; + break; + case AlphaComposite.DST_IN: + break; + case AlphaComposite.DST_OUT: + break; + case AlphaComposite.SRC_IN: + break; + case AlphaComposite.SRC_OUT: + break; + case AlphaComposite.DST_OVER: + break; + } + state.dictExtendedState.setItem(COSName.BM, blendMode); + } + + private Point2D clonePoint(Point2D point2D) { + return new Point2D.Double(point2D.getX(), point2D.getY()); + } + + /** + * Very small number, everything smaller than this is zero for us. + */ + private static final double EPSILON = 0.00001; + + private PDShading buildLinearGradientShading(Paint paint, PaintApplierState state) + throws IOException { + /* + * Batik has a copy of RadialGradientPaint, but it has the same structure as the AWT RadialGradientPaint. So we use + * Reflection to access the fields of both these classes. + */ + boolean isBatikGradient = paint.getClass().getPackage().getName() + .equals("org.apache.batik.ext.awt"); + boolean isObjectBoundingBox = false; + if (isBatikGradient) { + AffineTransform gradientTransform = getPropertyValue(paint, "getTransform"); + if (!gradientTransform.isIdentity()) { + /* + * If the scale is not square, we need to use the object bounding box logic + */ + if (Math.abs(gradientTransform.getScaleX() - gradientTransform.getScaleY()) + > EPSILON) + isObjectBoundingBox = true; + } + } + + if (isObjectBoundingBox) { + return linearGradientObjectBoundingBoxShading(paint, state); + } else { + return linearGradientUserSpaceOnUseShading(paint, state); + } + } + + private PDShading linearGradientObjectBoundingBoxShading(Paint paint, PaintApplierState state) + throws IOException { + /* + * I found this Stack Overflow question to be useful: https://stackoverflow.com/questions/50617275/svg-linear-gradients- + * objectboundingbox-vs-userspaceonuse SVG has 2 different gradient display modes objectBoundingBox & userSpaceOnUse The + * default is objectBoundingBox. PDF Axial gradients seem to be capable of displaying in any manner, but the default is + * the normal rendered at a 90 degree angle from the gradient vector. This looks like an SVG in userSpaceOnUse mode. So + * the task becomes how can we map the default of one format to a non-default mode in another so that the PDF an axial + * gradient looks like an SVG with a linear gradient. + * + * The approach I've used is as follows: Draw the axial gradient on a 1x1 box. A perfect square is a special case where + * the PDF defaults display matches the SVG default display. Then, use the gradient transform attached to the paint to + * warp the space containing the box & distort it to a larger rectangle (which may, or may not, still be a square). This + * makes the gradient in the PDF look like the gradient in an SVG if the SVG is using the objectBoundingBox mode. + * + * Note: there is some trickery with shape inversion because SVGs lay out from the top down & PDFs lay out from the + * bottom up. + */ + PDShadingType3 shading = setupBasicLinearShading(paint, state); + + Point2D startPoint = clonePoint(getPropertyValue(paint, "getStartPoint")); + Point2D endPoint = clonePoint(getPropertyValue(paint, "getEndPoint")); + AffineTransform gradientTransform = getPropertyValue(paint, "getTransform"); + state.tf.concatenate(gradientTransform); + + // noinspection unused + MultipleGradientPaint.CycleMethod cycleMethod = getCycleMethod(paint); + // noinspection unused + MultipleGradientPaint.ColorSpaceType colorSpaceType = getColorSpaceType(paint); + + // Note: all of the start and end points I've seen for linear gradients + // that use the objectBoundingBox mode define a 1x1 box. I don't know if + // this can be guaranteed. + setupShadingCoords(shading, startPoint, endPoint); + + // We need the rectangle here so that the call to clip(useEvenOdd) + // in PdfBoxGraphics2D.java clips to the right frame of reference + // + // Note: tricky stuff follows . . . + // We're deliberately creating a bounding box with a negative height. + // Why? Because that contentsStream.transform() is going to invert it + // so that it has a positive height. It will always invert because + // SVGs & PDFs have opposite layout directions. + // If we started with a positive height, then inverted to a negative height + // we end up with a negative height clipping box in the output PDF + // and some PDF viewers cannot handle that. + // e.g. Adobe acrobat will display the PDF one way & Mac Preview + // will display it another. + float calculatedX = (float) Math.min(startPoint.getX(), endPoint.getX()); + float calculatedY = (float) Math.max(1.0f, Math.max(startPoint.getY(), endPoint.getY())); + float calculatedWidth = Math + .max(1.0f, Math.abs((float) (endPoint.getX() - startPoint.getX()))); + float negativeHeight = + -1.0f * Math.max(1.0f, Math.abs((float) (endPoint.getY() - startPoint.getY()))); + + state.contentStream.addRect(calculatedX, calculatedY, calculatedWidth, negativeHeight); + + state.env.getGraphics2D().markPathIsOnStream(); + state.env.getGraphics2D().internalClip(false); + + // Warp the 1x1 box containing the gradient to fill a larger rectangular space + state.contentStream.transform(new Matrix(state.tf)); + + return shading; + } + + private void setupShadingCoords(PDShadingType3 shading, Point2D startPoint, Point2D endPoint) { + COSArray coords = new COSArray(); + coords.add(new COSFloat((float) startPoint.getX())); + coords.add(new COSFloat((float) startPoint.getY())); + coords.add(new COSFloat((float) endPoint.getX())); + coords.add(new COSFloat((float) endPoint.getY())); + shading.setCoords(coords); + } + + /** + * This is the default gradient mode for both SVG and java.awt gradients. + */ + private PDShading linearGradientUserSpaceOnUseShading(Paint paint, PaintApplierState state) + throws IOException { + + PDShadingType3 shading = setupBasicLinearShading(paint, state); + + Point2D startPoint = clonePoint(getPropertyValue(paint, "getStartPoint")); + Point2D endPoint = clonePoint(getPropertyValue(paint, "getEndPoint")); + AffineTransform gradientTransform = getPropertyValue(paint, "getTransform"); + state.tf.concatenate(gradientTransform); + + // noinspection unused + MultipleGradientPaint.CycleMethod cycleMethod = getCycleMethod(paint); + // noinspection unused + MultipleGradientPaint.ColorSpaceType colorSpaceType = getColorSpaceType(paint); + + state.tf.transform(startPoint, startPoint); + state.tf.transform(endPoint, endPoint); + + setupShadingCoords(shading, startPoint, endPoint); + + return shading; + } + + private PDShadingType3 setupBasicLinearShading(Paint paint, PaintApplierState state) + throws IOException { + PDShadingType3 shading = new PDShadingType3(new COSDictionary()); + Color[] colors = getPropertyValue(paint, "getColors"); + Color firstColor = colors[0]; + PDColor firstColorMapped = state.colorMapper.mapColor(state.contentStream, firstColor); + applyAsStrokingColor(firstColor, state); + float[] fractions = getPropertyValue(paint, "getFractions"); + PDFunctionType3 type3 = buildType3Function(colors, fractions, state); + shading.setAntiAlias(true); + shading.setShadingType(PDShading.SHADING_TYPE2); + shading.setColorSpace(firstColorMapped.getColorSpace()); + shading.setFunction(type3); + shading.setExtend(setupExtends()); + return shading; + } + + private COSArray setupExtends() { + COSArray extend = new COSArray(); + /* + * We need to always extend the gradient + */ + extend.add(COSBoolean.TRUE); + extend.add(COSBoolean.TRUE); + return extend; + } + + /** + * Map the cycleMethod of the GradientPaint to the java.awt.MultipleGradientPaint.CycleMethod enum. + * + * @param paint the paint to get the cycleMethod from (if not in any other way possible using reflection) + * @return the CycleMethod + */ + private MultipleGradientPaint.CycleMethod getCycleMethod(Paint paint) { + if (paint instanceof MultipleGradientPaint) + return ((MultipleGradientPaint) paint).getCycleMethod(); + if (paint.getClass().getPackage().getName().equals("org.apache.batik.ext.awt")) { + setupBatikReflectionAccess(paint); + Object cycleMethod = getPropertyValue(paint, "getCycleMethod"); + if (cycleMethod == BATIK_GRADIENT_NO_CYCLE) + return MultipleGradientPaint.CycleMethod.NO_CYCLE; + if (cycleMethod == BATIK_GRADIENT_REFLECT) + return MultipleGradientPaint.CycleMethod.REFLECT; + if (cycleMethod == BATIK_GRADIENT_REPEAT) + return MultipleGradientPaint.CycleMethod.REPEAT; + } + return MultipleGradientPaint.CycleMethod.NO_CYCLE; + } + + private MultipleGradientPaint.ColorSpaceType getColorSpaceType(Paint paint) { + if (paint instanceof MultipleGradientPaint) + return ((MultipleGradientPaint) paint).getColorSpace(); + if (paint.getClass().getPackage().getName().equals("org.apache.batik.ext.awt")) { + setupBatikReflectionAccess(paint); + Object cycleMethod = getPropertyValue(paint, "getColorSpace"); + if (cycleMethod == BATIK_COLORSPACE_SRGB) + return MultipleGradientPaint.ColorSpaceType.SRGB; + if (cycleMethod == BATIK_COLORSPACE_LINEAR_RGB) + return MultipleGradientPaint.ColorSpaceType.LINEAR_RGB; + } + return MultipleGradientPaint.ColorSpaceType.SRGB; + } + + private Object BATIK_GRADIENT_NO_CYCLE; + private Object BATIK_GRADIENT_REFLECT; + private Object BATIK_GRADIENT_REPEAT; + private Object BATIK_COLORSPACE_SRGB; + private Object BATIK_COLORSPACE_LINEAR_RGB; + + private void setupBatikReflectionAccess(Paint paint) { + /* + * As we don't have Batik on our class path we need to access it by reflection if the user application is using Batik + */ + if (BATIK_GRADIENT_NO_CYCLE != null) + return; + + try { + Class cls = paint.getClass(); + if (cls.getSimpleName().equals("MultipleGradientPaint")) { + BATIK_GRADIENT_NO_CYCLE = cls.getDeclaredField("NO_CYCLE"); + BATIK_GRADIENT_REFLECT = cls.getDeclaredField("REFLECT"); + BATIK_GRADIENT_REPEAT = cls.getDeclaredField("REPEAT"); + BATIK_COLORSPACE_SRGB = cls.getDeclaredField("SRGB"); + BATIK_COLORSPACE_LINEAR_RGB = cls.getDeclaredField("LINEAR_RGB"); + } + } catch (NoSuchFieldException ignored) { + /* + * Can not detect Batik CycleMethods :( + */ + } + } + + private PDShading buildRadialGradientShading(Paint paint, PaintApplierState state) + throws IOException { + /* + * Batik has a copy of RadialGradientPaint, but it has the same structure as the AWT RadialGradientPaint. So we use + * Reflection to access the fields of both these classes. + */ + Color[] colors = getPropertyValue(paint, "getColors"); + Color firstColor = colors[0]; + PDColor firstColorMapped = state.colorMapper.mapColor(state.contentStream, firstColor); + applyAsStrokingColor(firstColor, state); + + PDShadingType3 shading = new PDShadingType3(new COSDictionary()); + shading.setAntiAlias(true); + shading.setShadingType(PDShading.SHADING_TYPE3); + shading.setColorSpace(firstColorMapped.getColorSpace()); + float[] fractions = getPropertyValue(paint, "getFractions"); + Point2D centerPoint = clonePoint(getPropertyValue(paint, "getCenterPoint")); + Point2D focusPoint = clonePoint(getPropertyValue(paint, "getFocusPoint")); + AffineTransform gradientTransform = getPropertyValue(paint, "getTransform"); + state.tf.concatenate(gradientTransform); + state.tf.transform(centerPoint, centerPoint); + state.tf.transform(focusPoint, focusPoint); + + float radius = getPropertyValue(paint, "getRadius"); + radius = (float) Math.abs(radius * state.tf.getScaleX()); + + COSArray coords = new COSArray(); + + coords.add(new COSFloat((float) centerPoint.getX())); + coords.add(new COSFloat((float) centerPoint.getY())); + coords.add(new COSFloat(0)); + coords.add(new COSFloat((float) focusPoint.getX())); + coords.add(new COSFloat((float) focusPoint.getY())); + coords.add(new COSFloat(radius)); + shading.setCoords(coords); + + PDFunctionType3 type3 = buildType3Function(colors, fractions, state); + + shading.setFunction(type3); + shading.setExtend(setupExtends()); + return shading; + } + + private PDShading buildGradientShading(GradientPaint gradientPaint, PaintApplierState state) + throws IOException { + Color[] colors = new Color[]{gradientPaint.getColor1(), gradientPaint.getColor2()}; + Color firstColor = colors[0]; + PDColor firstColorMapped = state.colorMapper.mapColor(state.contentStream, firstColor); + applyAsStrokingColor(firstColor, state); + + PDShadingType3 shading = new PDShadingType3(new COSDictionary()); + shading.setShadingType(PDShading.SHADING_TYPE2); + shading.setColorSpace(firstColorMapped.getColorSpace()); + float[] fractions = new float[]{0, 1}; + PDFunctionType3 type3 = buildType3Function(colors, fractions, state); + + Point2D startPoint = gradientPaint.getPoint1(); + Point2D endPoint = gradientPaint.getPoint2(); + + state.tf.transform(startPoint, startPoint); + state.tf.transform(endPoint, endPoint); + + setupShadingCoords(shading, startPoint, endPoint); + + shading.setFunction(type3); + shading.setExtend(setupExtends()); + return shading; + } + + private void applyTexturePaint(TexturePaint texturePaint, PaintApplierState state) + throws IOException { + Rectangle2D anchorRect = texturePaint.getAnchorRect(); + PDTilingPattern pattern = new PDTilingPattern(); + pattern.setPaintType(PDTilingPattern.PAINT_COLORED); + pattern.setTilingType(PDTilingPattern.TILING_CONSTANT_SPACING_FASTER_TILING); + + pattern.setBBox(new PDRectangle((float) anchorRect.getX(), (float) anchorRect.getY(), + (float) anchorRect.getWidth(), (float) anchorRect.getHeight())); + pattern.setXStep((float) anchorRect.getWidth()); + pattern.setYStep((float) anchorRect.getHeight()); + + AffineTransform patternTransform = new AffineTransform(); + patternTransform.translate(0, anchorRect.getHeight()); + patternTransform.scale(1f, -1f); + pattern.setMatrix(patternTransform); + + PDAppearanceStream appearance = new PDAppearanceStream(state.document); + appearance.setResources(pattern.getResources()); + appearance.setBBox(pattern.getBBox()); + + PDPageContentStream imageContentStream = new PDPageContentStream(state.document, appearance, + ((COSStream) pattern.getCOSObject()).createOutputStream()); + BufferedImage texturePaintImage = texturePaint.getImage(); + PDImageXObject imageXObject = state.imageEncoder + .encodeImage(state.document, imageContentStream, texturePaintImage); + + float ratioW = (float) ((anchorRect.getWidth()) / texturePaintImage.getWidth()); + float ratioH = (float) ((anchorRect.getHeight()) / texturePaintImage.getHeight()); + float paintHeight = (texturePaintImage.getHeight()) * ratioH; + if (state.nestedTransform != null) { + imageContentStream.transform(new Matrix(state.nestedTransform)); + } + imageContentStream.drawImage(imageXObject, (float) anchorRect.getX(), + (float) (paintHeight + anchorRect.getY()), texturePaintImage.getWidth() * ratioW, + -paintHeight); + imageContentStream.close(); + + PDColorSpace patternCS1 = new PDPattern(null, imageXObject.getColorSpace()); + COSName tilingPatternName = state.resources.add(pattern); + PDColor patternColor = new PDColor(tilingPatternName, patternCS1); + + state.contentStream.setNonStrokingColor(patternColor); + state.contentStream.setStrokingColor(patternColor); + } + + /** + * Encode a color gradient as a type3 function + * + * @param colors The colors to encode + * @param fractions the fractions for encoding + * @param state our state, this is needed for color mapping + * @return the type3 function + */ + private PDFunctionType3 buildType3Function(Color[] colors, float[] fractions, + PaintApplierState state) { + COSDictionary function = new COSDictionary(); + function.setInt(COSName.FUNCTION_TYPE, 3); + + COSArray domain = new COSArray(); + domain.add(new COSFloat(0)); + domain.add(new COSFloat(1)); + + COSArray encode = new COSArray(); + + COSArray range = new COSArray(); + range.add(new COSFloat(0)); + range.add(new COSFloat(1)); + + List colorList = new ArrayList(Arrays.asList(colors)); + COSArray bounds = new COSArray(); + if (Math.abs(fractions[0]) > EPSILON) { + /* + * We need to insert a "keyframe" for fraction 0. See also java.awt.LinearGradientPaint for future information + */ + colorList.add(0, colors[0]); + bounds.add(new COSFloat(fractions[0])); + } + + /* + * We always add the inner fractions + */ + for (int i = 1; i < fractions.length - 1; i++) { + float fraction = fractions[i]; + bounds.add(new COSFloat(fraction)); + } + if (Math.abs(fractions[fractions.length - 1] - 1f) > EPSILON) { + /* + * We also need to insert a "keyframe" at the end for fraction 1 + */ + colorList.add(colors[colors.length - 1]); + bounds.add(new COSFloat(fractions[fractions.length - 1])); + } + + COSArray type2Functions = buildType2Functions(colorList, domain, encode, state); + + function.setItem(COSName.FUNCTIONS, type2Functions); + function.setItem(COSName.BOUNDS, bounds); + function.setItem(COSName.ENCODE, encode); + + PDFunctionType3 type3 = new PDFunctionType3(function); + type3.setDomainValues(domain); + return type3; + } + + /** + * Build a type2 function to interpolate between the given colors. + * + * @param colors the color to encode + * @param domain the domain which should already been setuped. It will be used for the Type2 function + * @param encode will get the domain information per color channel, i.e. colors.length x [0, 1] + * @param state our internal state, this is needed for color mapping + * @return the Type2 function COSArray + */ + private COSArray buildType2Functions(List colors, COSArray domain, COSArray encode, + PaintApplierState state) { + Color prevColor = colors.get(0); + + COSArray functions = new COSArray(); + for (int i = 1; i < colors.size(); i++) { + Color color = colors.get(i); + PDColor prevPdColor = state.colorMapper.mapColor(state.contentStream, prevColor); + PDColor pdColor = state.colorMapper.mapColor(state.contentStream, color); + COSArray c0 = new COSArray(); + COSArray c1 = new COSArray(); + for (float component : prevPdColor.getComponents()) + c0.add(new COSFloat(component)); + for (float component : pdColor.getComponents()) + c1.add(new COSFloat(component)); + + COSDictionary type2Function = new COSDictionary(); + type2Function.setInt(COSName.FUNCTION_TYPE, 2); + type2Function.setItem(COSName.C0, c0); + type2Function.setItem(COSName.C1, c1); + type2Function.setInt(COSName.N, 1); + type2Function.setItem(COSName.DOMAIN, domain); + functions.add(type2Function); + + encode.add(new COSFloat(0)); + encode.add(new COSFloat(1)); + prevColor = color; + } + return functions; + } + + /** + * Get a property value from an object using reflection + * + * @param obj The object to get a property from. + * @param propertyGetter method name of the getter, i.e. "getXY". + * @param the type of the property you want to get. + * @return the value read from the object + */ + @SuppressWarnings("unchecked") + protected static T getPropertyValue(Object obj, String propertyGetter) { + if (obj == null) { + return null; + } + Class c = obj.getClass(); + while (c != null) { + try { + Method m = c.getMethod(propertyGetter, (Class[]) null); + return (T) m.invoke(obj); + } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { + logger.log(Level.WARNING, e.getMessage(), e); + } + c = c.getSuperclass(); + } + throw new NullPointerException("Method " + propertyGetter + " not found on object " + obj.getClass().getName()); + } + + private static abstract class COSResourceCacheBase { + private final Map> states = new HashMap<>(); + + private static boolean equalsCOSDictionary(COSDictionary cosDictionary, + COSDictionary cosDictionary1) { + if (cosDictionary.size() != cosDictionary1.size()) + return false; + for (COSName name : cosDictionary.keySet()) { + COSBase item = cosDictionary.getItem(name); + COSBase item2 = cosDictionary1.getItem(name); + if (!equalsCOSBase(item, item2)) + return false; + } + return true; + } + + private static boolean equalsCOSBase(COSBase item, COSBase item2) { + if (item == item2) + return true; + if (item == null) + return false; + if (item2 == null) + return false; + /* + * Can the items be compared directly? + */ + if (item.equals(item2)) + return true; + + if (item instanceof COSDictionary && item2 instanceof COSDictionary) + return equalsCOSDictionary((COSDictionary) item, (COSDictionary) item2); + + // noinspection SimplifiableIfStatement + if (item instanceof COSArray && item2 instanceof COSArray) + return equalsCOSArray((COSArray) item, (COSArray) item2); + + return false; + } + + private static boolean equalsCOSArray(COSArray item, COSArray item2) { + if (item.size() != item2.size()) + return false; + for (int i = 0; i < item.size(); i++) { + COSBase i1 = item.getObject(i); + COSBase i2 = item2.getObject(i); + if (!equalsCOSBase(i1, i2)) + return false; + } + return true; + } + + protected abstract int getKey(TObject obj); + + TObject makeUnqiue(TObject state) { + int key = getKey(state); + List pdExtendedGraphicsStates = states.get(key); + if (pdExtendedGraphicsStates == null) { + pdExtendedGraphicsStates = new ArrayList(); + states.put(key, pdExtendedGraphicsStates); + } + for (TObject s : pdExtendedGraphicsStates) { + if (stateEquals(s, state)) + return s; + } + pdExtendedGraphicsStates.add(state); + return state; + } + + private boolean stateEquals(TObject s, TObject state) { + COSBase base1 = s.getCOSObject(); + COSBase base2 = state.getCOSObject(); + return equalsCOSBase(base1, base2); + } + } + + private static class ExtGStateCache extends COSResourceCacheBase { + @Override + protected int getKey(PDExtendedGraphicsState obj) { + return obj.getCOSObject().size(); + } + } + + private static class PDShadingCache extends COSResourceCacheBase { + @Override + protected int getKey(PDShading obj) { + return obj.getCOSObject().size(); + } + } + +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DrawControl.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DrawControl.java new file mode 100644 index 0000000..e7ff0ce --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/DrawControl.java @@ -0,0 +1,71 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import java.awt.Paint; +import java.awt.Shape; + +/** + * Allows you to influence the fill and draw operations. You can alter the shape + * to draw/fill, you can even filter out the complete draw/fill operation. + * And you can draw additional stuff after the draw/fill operation, e.g. to + * implement overfill. + */ +public interface DrawControl { + /** + * You may optional change the shape that is going to be filled. You can also do + * other stuff here like drawing an overfill before the real shape. + * + * @param shape the shape that will be drawn + * @param env Environment + * @return the shape to be filled. If you return null, nothing will be filled + */ + Shape transformShapeBeforeFill(Shape shape, IDrawControlEnv env); + + /** + * You may optional change the shape that is going to be drawn. You can also do + * other stuff here like drawing an overfill before the real shape. + * + * @param shape the shape that will be drawn + * @param env Environment + * @return the shape to be filled. If you return null, nothing will be drawn + */ + Shape transformShapeBeforeDraw(Shape shape, IDrawControlEnv env); + + /** + * Called after shape was filled. This method is always called, even if + * {@link #transformShapeBeforeFill(Shape, IDrawControlEnv)} returns + * null. + * + * @param shape the shape that was filled. This is the original shape, not the one + * transformed by + * {@link #transformShapeBeforeFill(Shape, IDrawControlEnv)}. + * @param env Environment + */ + void afterShapeFill(Shape shape, IDrawControlEnv env); + + /** + * Called after shape was drawn. This method is always called, even if + * {@link #transformShapeBeforeDraw(Shape, IDrawControlEnv)} returns + * null. + * + * @param shape the shape that was drawn. This is the original shape, not the one + * transformed by + * {@link #transformShapeBeforeDraw(Shape, IDrawControlEnv)}. + * @param env Environment + */ + void afterShapeDraw(Shape shape, IDrawControlEnv env); + + /** + * The environment of the draw operation + */ + interface IDrawControlEnv { + /** + * @return the current paint set on the graphics. + */ + Paint getPaint(); + + /** + * @return the graphics currently drawn on + */ + PdfBoxGraphics2D getGraphics(); + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/FontTextDrawer.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/FontTextDrawer.java new file mode 100644 index 0000000..f84c455 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/FontTextDrawer.java @@ -0,0 +1,110 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; + +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.io.IOException; +import java.text.AttributedCharacterIterator; + +/** + * Draw text using Fonts + */ +public interface FontTextDrawer { + + /** + * Enviroment for font based drawing of text + */ + interface IFontTextDrawerEnv { + /** + * @return the document we are writing to + */ + PDDocument getDocument(); + + /** + * @return the content stream + */ + PDPageContentStream getContentStream(); + + /** + * @return the current font set on the graphics. This is the "default" font to + * use when no other font is set on the + * {@link AttributedCharacterIterator}. + */ + Font getFont(); + + /** + * @return the current paint set on the graphics. This is the "default" paint + * when no other paint is set on on the + * {@link AttributedCharacterIterator}. + */ + Paint getPaint(); + + /** + * Apply the given paint on the current content stream + * + * @param paint Paint to apply + * @param shapeToDraw the shape to draw of the text, if known. This is needed to + * calculate correct gradients. + * @throws IOException if an IO error occurs when writing the paint to the content + * stream. + */ + void applyPaint(Paint paint, Shape shapeToDraw) throws IOException; + + /** + * @return the {@link Graphics2D} {@link FontRenderContext} + */ + FontRenderContext getFontRenderContext(); + + /** + * @return the bbox of the {@link PdfBoxGraphics2D} + */ + PDRectangle getGraphicsBBox(); + + /** + * @return the resource of the content stream + */ + PDResources getResources(); + + /** + * @return the default calcuation BufferedImage based graphics. + */ + Graphics2D getCalculationGraphics(); + } + + /** + * @param iterator Has the text and all its properties + * @param env Environment + * @return true when the given text can be fully drawn using fonts. return false + * to have the text drawn as vector shapes + * @throws IOException when a font can not be loaded or a paint can't be applied. + * @throws FontFormatException when the font file can not be loaded + */ + boolean canDrawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) + throws IOException, FontFormatException; + + /** + * @param iterator The text with all properties + * @param env Environment + * @throws IOException when a font can not be loaded or a paint can't be applied. + * @throws FontFormatException when the font file can not be loaded + */ + void drawText(AttributedCharacterIterator iterator, IFontTextDrawerEnv env) + throws IOException, FontFormatException; + + /** + * @param f + * @param env + * @return + */ + FontMetrics getFontMetrics(Font f, IFontTextDrawerEnv env) + throws IOException, FontFormatException; +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/ImageEncoder.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/ImageEncoder.java new file mode 100644 index 0000000..d74e081 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/ImageEncoder.java @@ -0,0 +1,22 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; + +import java.awt.Image; + +/** + * Encode and compress an image as PDImageXObject + */ +public interface ImageEncoder { + /** + * Encode the given image into the a PDImageXObject + * + * @param document the PDF document + * @param contentStream the content stream of the page + * @param image the image to encode + * @return the encoded image + */ + PDImageXObject encodeImage(PDDocument document, PDPageContentStream contentStream, Image image); +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/LosslessImageEncoder.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/LosslessImageEncoder.java new file mode 100644 index 0000000..c88dd62 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/LosslessImageEncoder.java @@ -0,0 +1,126 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; + +import java.awt.Graphics; +import java.awt.Image; +import java.awt.color.ColorSpace; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.awt.image.BufferedImage; +import java.io.IOException; +import java.lang.ref.SoftReference; +import java.util.HashMap; +import java.util.Map; + +/** + * Encodes all images using lossless compression. Tries to reuse images as much + * as possible. You can share an instance of this class with multiple + * PdfBoxGraphics2D objects. + */ +public class LosslessImageEncoder implements ImageEncoder { + private Map> imageMap = new HashMap<>(); + private Map> profileMap = new HashMap<>(); + private SoftReference doc; + + @Override + public PDImageXObject encodeImage(PDDocument document, PDPageContentStream contentStream, Image image) { + final BufferedImage bi; + + if (image instanceof BufferedImage) { + bi = (BufferedImage) image; + } else { + int width = image.getWidth(null); + int height = image.getHeight(null); + bi = new BufferedImage(width, height, BufferedImage.TYPE_4BYTE_ABGR); + Graphics graphics = bi.getGraphics(); + if (!graphics.drawImage(image, 0, 0, null, null)) + throw new IllegalStateException("Not fully loaded images are not supported."); + graphics.dispose(); + } + + try { + if (doc == null || doc.get() != document) { + imageMap = new HashMap<>(); + profileMap = new HashMap<>(); + doc = new SoftReference<>(document); + } + SoftReference pdImageXObjectSoftReference = imageMap.get(new ImageSoftReference(image)); + PDImageXObject imageXObject = pdImageXObjectSoftReference == null ? null + : pdImageXObjectSoftReference.get(); + if (imageXObject == null) { + imageXObject = LosslessFactory.createFromImage(document, bi); + if (bi.getColorModel().getColorSpace() instanceof ICC_ColorSpace) { + ICC_Profile profile = ((ICC_ColorSpace) bi.getColorModel().getColorSpace()).getProfile(); + if (((ICC_ColorSpace) bi.getColorModel().getColorSpace()).getProfile() != ICC_Profile + .getInstance(ColorSpace.CS_sRGB)) { + SoftReference pdProfileRef = profileMap.get(new ProfileSoftReference(profile)); + PDColorSpace pdProfile = pdProfileRef == null ? null : pdProfileRef.get(); + if (pdProfile == null) { + pdProfile = imageXObject.getColorSpace(); + if (pdProfile instanceof PDICCBased) { + profileMap.put(new ProfileSoftReference(profile), + new SoftReference<>(pdProfile)); + } + } + imageXObject.setColorSpace(pdProfile); + } + } + imageMap.put(new ImageSoftReference(image), new SoftReference<>(imageXObject)); + } + + return imageXObject; + } catch (IOException e) { + throw new RuntimeException("Could not encode Image", e); + } + } + + private static class ImageSoftReference extends SoftReference { + ImageSoftReference(Image referent) { + super(referent); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + assert obj instanceof ImageSoftReference; + return ((ImageSoftReference) obj).get() == get(); + } + + @Override + public int hashCode() { + Image image = get(); + if (image == null) + return 0; + return image.hashCode(); + } + } + + private static class ProfileSoftReference extends SoftReference { + ProfileSoftReference(ICC_Profile referent) { + super(referent); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) + return false; + assert obj instanceof ProfileSoftReference; + return ((ProfileSoftReference) obj).get() == get(); + } + + @Override + public int hashCode() { + ICC_Profile image = get(); + if (image == null) + return 0; + return image.hashCode(); + } + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/PaintApplier.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/PaintApplier.java new file mode 100644 index 0000000..def14f8 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/PaintApplier.java @@ -0,0 +1,85 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; + +import java.awt.Color; +import java.awt.Composite; +import java.awt.Graphics2D; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.geom.AffineTransform; +import java.io.IOException; + +/** + * Apply the given paint on the Content Stream. + */ +public interface PaintApplier { + /** + * Apply the paint on the ContentStream + * + * @param paint the paint which should be applied + * @param contentStream the content stream to apply the paint on + * @param currentTransform the current transform of the Graphics2D relative to the + * contentStream default coordinate space. This is always a copy of the + * current transform, so we can modify it. + * @param env Environment for mapping the paint. + * @return null or a PDShading which should be used to fill a shape. + * @throws IOException if its not possible to write the paint into the contentStream + */ + PDShading applyPaint(Paint paint, PDPageContentStream contentStream, + AffineTransform currentTransform, IPaintEnv env) throws IOException; + + /** + * The different mappers used by the paint applier. This interface is + * implemented internally by {@link PdfBoxGraphics2D} + */ + interface IPaintEnv { + /** + * @return the color mapper + */ + ColorMapper getColorMapper(); + + /** + * @return the image encoder + */ + ImageEncoder getImageEncoder(); + + /** + * @return the document + */ + PDDocument getDocument(); + + /** + * @return the resource of the content stream + */ + PDResources getResources(); + + /** + * @return the {@link Graphics2D} {@link Composite} + */ + Composite getComposite(); + + /** + * @return The PdfBoxGraphics2D + */ + PdfBoxGraphics2D getGraphics2D(); + + /** + * @return the {@link Graphics2D} XOR Mode {@link Color} or null if paint mode + * is active. + */ + @SuppressWarnings("unused") + Color getXORMode(); + + /** + * The shape information is need to be able to correctly render grandients. + * + * @return get the shape which will be drawn or filled with this paint. Null is + * returned if no shape is known. + */ + Shape getShapeToDraw(); + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2D.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2D.java new file mode 100644 index 0000000..bce50a7 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2D.java @@ -0,0 +1,1322 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.xbib.graphics.graphics2d.pdfbox.DrawControl.IDrawControlEnv; +import org.xbib.graphics.graphics2d.pdfbox.FontTextDrawer.IFontTextDrawerEnv; +import org.xbib.graphics.graphics2d.pdfbox.PaintApplier.IPaintEnv; +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.cos.COSStream; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.PDResources; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDPattern; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.pdmodel.graphics.pattern.PDTilingPattern; +import org.apache.pdfbox.pdmodel.graphics.shading.PDShading; +import org.apache.pdfbox.pdmodel.interactive.annotation.PDAppearanceStream; +import org.apache.pdfbox.util.Matrix; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.awt.GraphicsConfiguration; +import java.awt.Image; +import java.awt.Paint; +import java.awt.Polygon; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.RenderingHints.Key; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.font.FontRenderContext; +import java.awt.font.GlyphVector; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Arc2D; +import java.awt.geom.Area; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Line2D; +import java.awt.geom.NoninvertibleTransformException; +import java.awt.geom.Path2D; +import java.awt.geom.PathIterator; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.awt.image.BufferedImageOp; +import java.awt.image.ImageObserver; +import java.awt.image.RenderedImage; +import java.awt.image.WritableRaster; +import java.awt.image.renderable.RenderableImage; +import java.io.IOException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Graphics 2D Adapter for PDFBox. + */ +public class PdfBoxGraphics2D extends Graphics2D { + + private final PDFormXObject xFormObject; + + private final Graphics2D calcGfx; + + private final PDPageContentStream contentStream; + + private BufferedImage calcImage; + + private PDDocument document; + + private final AffineTransform baseTransform; + + private AffineTransform transform = new AffineTransform(); + + private ImageEncoder imageEncoder = new LosslessImageEncoder(); + + private ColorMapper colorMapper = new DefaultColorMapper(); + + private PaintApplier paintApplier = new DefaultPaintApplier(); + + private FontTextDrawer fontTextDrawer = new DefaultFontTextDrawer(); + + private DrawControl drawControl = DefaultDrawControl.INSTANCE; + + private Paint paint; + + private Stroke stroke; + + private Color xorColor; + + private Font font; + + private Composite composite; + + private Shape clipShape; + + private Color backgroundColor; + + private final CopyInfo copyInfo; + + private final PDRectangle bbox; + + /** + * Set a new color mapper. + * + * @param colorMapper the color mapper which maps Color to PDColor. + */ + public void setColorMapper(ColorMapper colorMapper) { + this.colorMapper = colorMapper; + } + + /** + * Set a new image encoder + * + * @param imageEncoder the image encoder, which encodes a image as PDImageXForm. + */ + public void setImageEncoder(ImageEncoder imageEncoder) { + this.imageEncoder = imageEncoder; + } + + /** + * Set a new paint applier. You should always derive your custom paint applier + * from the {@link PaintApplier} and just extend the paint + * mapping for custom paint. + *

+ * If the paint you map is a paint from a standard library and you can implement + * the mapping using reflection please feel free to send a pull request to + * extend the default paint mapper. + * + * @param paintApplier the paint applier responsible for mapping the paint correctly + */ + public void setPaintApplier(PaintApplier paintApplier) { + this.paintApplier = paintApplier; + } + + /** + * Set a new draw control. This allows you to influence fill() and draw() + * operations. drawString() is only influence if the text is drawn as vector + * shape. + * + * @param drawControl the draw control + */ + public void setDrawControl(DrawControl drawControl) { + this.drawControl = drawControl; + } + + /** + * Create a PDfBox Graphics2D. This size is used for the BBox of the XForm. So + * everything drawn outside the rectangle (0x0)-(pixelWidth,pixelHeight) will be + * clipped. + *

+ * Note: pixelWidth and pixelHeight only define the size of the coordinate space + * within this Graphics2D. They do not affect how big the XForm is finally + * displayed in the PDF. + * + * @param document The document the graphics should be used to create a XForm in. + * @param pixelWidth the width in pixel of the drawing area. + * @param pixelHeight the height in pixel of the drawing area. + * @throws IOException if something goes wrong with writing into the content stream of + * the {@link PDDocument}. + */ + public PdfBoxGraphics2D(PDDocument document, int pixelWidth, int pixelHeight) throws IOException { + this(document, new PDRectangle(pixelWidth, pixelHeight)); + } + + /** + * Create a PDfBox Graphics2D. This size is used for the BBox of the XForm. So + * everything drawn outside the rectangle (0x0)-(pixelWidth,pixelHeight) will be + * clipped. + *

+ * Note: pixelWidth and pixelHeight only define the size of the coordinate space + * within this Graphics2D. They do not affect how big the XForm is finally + * displayed in the PDF. + * + * @param document The document the graphics should be used to create a XForm in. + * @param pixelWidth the width in pixel of the drawing area. + * @param pixelHeight the height in pixel of the drawing area. + * @throws IOException if something goes wrong with writing into the content stream of + * the {@link PDDocument}. + */ + public PdfBoxGraphics2D(PDDocument document, float pixelWidth, float pixelHeight) + throws IOException { + this(document, new PDRectangle(pixelWidth, pixelHeight)); + } + + /** + * Set an optional text drawer. By default, all text is vectorized and drawn + * using vector shapes. To embed fonts into a PDF file it is necessary to have + * the underlying TTF file. The java.awt.Font class does not provide that. The + * FontTextDrawer must perform the java.awt.Font <=> PDFont mapping and + * also must perform the text layout. If it can not map the text or font + * correctly, the font drawing falls back to vectoring the text. + * + * @param fontTextDrawer The text drawer, which can draw text using fonts + */ + @SuppressWarnings("WeakerAccess") + public void setFontTextDrawer(FontTextDrawer fontTextDrawer) { + this.fontTextDrawer = fontTextDrawer; + } + + private int saveCounter = 0; + + private final List copyList = new ArrayList(); + + private static class CopyInfo { + PdfBoxGraphics2D sourceGfx; + PdfBoxGraphics2D copy; + String creatingContextInfo; + + @Override + public String toString() { + return "CopyInfo{creatingContextInfo='" + creatingContextInfo + '\'' + '}'; + } + } + + /** + * @param document The document the graphics should be used to create a XForm in. + * @param bbox Bounding Box of the graphics + * @throws IOException when something goes wrong with writing into the content stream of + * the {@link PDDocument}. + */ + public PdfBoxGraphics2D(PDDocument document, PDRectangle bbox) throws IOException { + this(document, bbox, null); + } + + /* + * @internal + */ + PdfBoxGraphics2D(PDDocument document, PDRectangle bbox, PdfBoxGraphics2D parentGfx) + throws IOException { + this.document = document; + this.bbox = bbox; + + PDAppearanceStream appearance = new PDAppearanceStream(document); + xFormObject = appearance; + xFormObject.setResources(new PDResources()); + xFormObject.setBBox(bbox); + contentStream = new PDPageContentStream(document, appearance, + xFormObject.getStream().createOutputStream(COSName.FLATE_DECODE)); + contentStreamSaveState(); + + if (parentGfx != null) { + this.colorMapper = parentGfx.colorMapper; + this.fontTextDrawer = parentGfx.fontTextDrawer; + this.imageEncoder = parentGfx.imageEncoder; + this.paintApplier = parentGfx.paintApplier; + } + + baseTransform = new AffineTransform(); + baseTransform.translate(0, bbox.getHeight()); + baseTransform.scale(1, -1); + + calcImage = new BufferedImage(100, 100, BufferedImage.TYPE_4BYTE_ABGR); + calcGfx = calcImage.createGraphics(); + font = calcGfx.getFont(); + copyInfo = null; + + } + + /** + * @return the PDAppearanceStream which resulted in this graphics + */ + @SuppressWarnings("WeakerAccess") + public PDFormXObject getXFormObject() { + if (document != null) + throw new IllegalStateException( + "You can only get the XformObject after you disposed the Graphics2D!"); + if (copyInfo != null) + throw new IllegalStateException("You can not get the Xform stream from the copy"); + return xFormObject; + } + + private PdfBoxGraphics2D(PdfBoxGraphics2D gfx) throws IOException { + CopyInfo info = new CopyInfo(); + info.creatingContextInfo = gatherContext(); + info.copy = this; + info.sourceGfx = gfx; + gfx.copyList.add(info); + this.copyInfo = info; + this.hasPathOnStream = false; + this.document = gfx.document; + this.bbox = gfx.bbox; + this.xFormObject = gfx.xFormObject; + this.contentStream = gfx.contentStream; + this.baseTransform = gfx.baseTransform; + this.transform = (AffineTransform) gfx.transform.clone(); + this.calcGfx = gfx.calcGfx; + this.calcImage = gfx.calcImage; + this.font = gfx.font; + this.stroke = gfx.stroke; + this.paint = gfx.paint; + this.clipShape = gfx.clipShape; + this.backgroundColor = gfx.backgroundColor; + this.colorMapper = gfx.colorMapper; + this.fontTextDrawer = gfx.fontTextDrawer; + this.imageEncoder = gfx.imageEncoder; + this.paintApplier = gfx.paintApplier; + this.drawControl = gfx.drawControl; + this.composite = gfx.composite; + this.renderingHints = new HashMap(gfx.renderingHints); + this.xorColor = gfx.xorColor; + this.saveCounter = 0; + + contentStreamSaveState(); + } + + /** + * Global Flag: If set to true the Callstack when creating a + * context is recorded. + *

+ * Note: Setting this to true will slow down the library. Use this only for + * debugging. + */ + public static boolean ENABLE_CHILD_CREATING_DEBUG = false; + + private String gatherContext() { + if (!ENABLE_CHILD_CREATING_DEBUG) + return null; + StackTraceElement[] stackTrace = Thread.currentThread().getStackTrace(); + StringBuilder sb = new StringBuilder(); + for (StackTraceElement stackTraceElement : stackTrace) { + if (stackTraceElement.getClassName().startsWith("de.rototor.pdfbox")) + continue; + if (stackTraceElement.getClassName().startsWith("org.junit")) + continue; + if (stackTraceElement.getClassName().startsWith("com.intellij.rt")) + continue; + if (stackTraceElement.getClassName().startsWith("java.lang")) + continue; + sb.append(" at ").append(stackTraceElement.getClassName()).append(".") + .append(stackTraceElement.getMethodName()).append("(") + .append(stackTraceElement.getFileName()).append(":") + .append(stackTraceElement.getLineNumber()).append(")").append("\n"); + } + return sb.toString(); + } + + /** + * Sometimes the users of {@link #create()} don't correctly {@link #dispose()} + * the child graphics they create. And you may not always be able to fix this + * uses, as it may be in some 3rdparty library. In this case this method can + * help you. It will cleanup all dangling child graphics. The child graphics can + * not be used after that. This method is a workaround for a buggy old code. You + * should only use it if you have to.
+ *

+ * Note: You can only call this method on the "main" graphics, not on a child + * created with {@link #create()} + */ + @SuppressWarnings("WeakerAccess") + public void disposeDanglingChildGraphics() { + if (copyInfo != null) + throw new IllegalStateException( + "Don't call disposeDanglingChildGraphics() on a child!"); + disposeCopies(copyList); + } + + private static void disposeCopies(List cl) { + while (cl.size() > 0) { + CopyInfo copyInfo = cl.get(0); + disposeCopies(copyInfo.copy.copyList); + copyInfo.copy.dispose(); + } + } + + public void dispose() { + if (copyInfo != null) { + copyInfo.sourceGfx.copyList.remove(copyInfo); + try { + contentStreamRestoreState(); + } catch (IOException e) { + throwException(e); + } + if (this.saveCounter != 0) + throw new IllegalStateException( + "Copy - SaveCounter should be 0, but is " + this.saveCounter); + return; + } + if (copyList.size() > 0) + /* + * When not all copies created by create() are disposed(), the resulting PDF + * content stream will be invalid, as the save/restore context commands (q/Q) + * are not balanced. You should always dispose() a graphics context when you are + * done with it. + */ + throw new IllegalStateException( + "Not all PdfGraphics2D copies were destroyed! Please ensure that all create() calls get a matching dispose() on the returned copies. Also consider using disposeDanglingChildGraphics()"); + try { + contentStreamRestoreState(); + contentStream.close(); + } catch (IOException e) { + throwException(e); + } + if (this.saveCounter != 0) + throw new IllegalStateException("SaveCounter should be 0, but is " + this.saveCounter); + + document = null; + calcGfx.dispose(); + calcImage.flush(); + calcImage = null; + } + + private final IDrawControlEnv drawControlEnv = new IDrawControlEnv() { + @Override + public Paint getPaint() { + return paint; + } + + @Override + public PdfBoxGraphics2D getGraphics() { + return PdfBoxGraphics2D.this; + } + }; + + public void draw(Shape s) { + checkNoCopyActive(); + /* + * Don't try to draw with no paint, just ignore that. + */ + if (paint == null) + return; + try { + contentStreamSaveState(); + + Shape shapeToDraw = drawControl.transformShapeBeforeDraw(s, drawControlEnv); + + if (shapeToDraw != null) { + walkShape(shapeToDraw); + PDShading pdShading = applyPaint(shapeToDraw); + if (pdShading != null) + applyShadingAsColor(pdShading); + + if (stroke instanceof BasicStroke) { + BasicStroke basicStroke = (BasicStroke) this.stroke; + + // Cap Style maps 1:1 between Java and PDF Spec + contentStream.setLineCapStyle(basicStroke.getEndCap()); + // Line Join Style maps 1:1 between Java and PDF Spec + contentStream.setLineJoinStyle(basicStroke.getLineJoin()); + if (basicStroke.getMiterLimit() > 0) { + // Also Miter maps 1:1 between Java and PDF Spec + // (NB: set the miter-limit only if value is > 0) + contentStream.setMiterLimit(basicStroke.getMiterLimit()); + } + + AffineTransform tf = new AffineTransform(); + tf.concatenate(baseTransform); + tf.concatenate(transform); + + double scaleX = tf.getScaleX(); + contentStream + .setLineWidth((float) Math.abs(basicStroke.getLineWidth() * scaleX)); + float[] dashArray = basicStroke.getDashArray(); + if (dashArray != null) { + for (int i = 0; i < dashArray.length; i++) + dashArray[i] = (float) Math.abs(dashArray[i] * scaleX); + contentStream.setLineDashPattern(dashArray, + (float) Math.abs(basicStroke.getDashPhase() * scaleX)); + } + } + + contentStream.stroke(); + hasPathOnStream = false; + } + + drawControl.afterShapeDraw(s, drawControlEnv); + + contentStreamRestoreState(); + } catch (IOException e) { + throwException(e); + } + } + + public void drawImage(BufferedImage img, BufferedImageOp op, int x, int y) { + BufferedImage img1 = op.filter(img, null); + drawImage(img1, new AffineTransform(1f, 0f, 0f, 1f, x, y), null); + } + + public void drawRenderedImage(RenderedImage img, AffineTransform xform) { + WritableRaster data = img.copyData(null); + drawImage(new BufferedImage(img.getColorModel(), data, false, null), xform, null); + } + + public void drawRenderableImage(RenderableImage img, AffineTransform xform) { + drawRenderedImage(img.createDefaultRendering(), xform); + } + + public void drawString(String str, int x, int y) { + drawString(str, (float) x, (float) y); + } + + public void drawString(String str, float x, float y) { + AttributedString attributedString = new AttributedString(str); + attributedString.addAttribute(TextAttribute.FONT, font); + drawString(attributedString.getIterator(), x, y); + } + + public void drawString(AttributedCharacterIterator iterator, int x, int y) { + drawString(iterator, (float) x, (float) y); + } + + public boolean drawImage(Image img, int x, int y, ImageObserver observer) { + return drawImage(img, x, y, img.getWidth(observer), img.getHeight(observer), observer); + } + + public boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) { + AffineTransform tf = new AffineTransform(); + tf.translate(x, y); + tf.scale((float) width / img.getWidth(null), (float) height / img.getHeight(null)); + return drawImage(img, tf, observer); + } + + public boolean drawImage(Image img, int x, int y, Color bgcolor, ImageObserver observer) { + return drawImage(img, x, y, img.getWidth(observer), img.getHeight(observer), bgcolor, + observer); + } + + public boolean drawImage(Image img, int x, int y, int width, int height, Color bgcolor, + ImageObserver observer) { + try { + if (bgcolor != null) { + contentStream.setNonStrokingColor(colorMapper.mapColor(contentStream, bgcolor)); + walkShape(new Rectangle(x, y, width, height)); + contentStream.fill(); + } + return drawImage(img, x, y, img.getWidth(observer), img.getHeight(observer), observer); + } catch (IOException e) { + throwException(e); + return false; + } + } + + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, + int sx2, int sy2, ImageObserver observer) { + return drawImage(img, dx1, dy1, dx2, dy2, sx1, sy2, sx2, sy2, null, observer); + } + + public boolean drawImage(Image img, AffineTransform xform, ImageObserver obs) { + checkNoCopyActive(); + AffineTransform tf = new AffineTransform(); + tf.concatenate(baseTransform); + tf.concatenate(transform); + + // Sometimes the xform can be null + if (xform != null) + tf.concatenate((AffineTransform) xform.clone()); + + PDImageXObject pdImage = imageEncoder.encodeImage(document, contentStream, img); + try { + contentStreamSaveState(); + int imgHeight = img.getHeight(obs); + tf.translate(0, imgHeight); + tf.scale(1, -1); + contentStream.transform(new Matrix(tf)); + + Object keyInterpolation = renderingHints.get(RenderingHints.KEY_INTERPOLATION); + if (RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR.equals(keyInterpolation)) + pdImage.setInterpolate(false); + + if (composite != null) { + // We got an AlphaComposite, we must set the extended graphics dictionary correctly. + // The PaintApplyer will do this for us. So we just apply the current paint + // so that the graphics dictionary is set correctly. + applyPaint(null); + } + + contentStream.drawImage(pdImage, 0, 0, img.getWidth(obs), imgHeight); + contentStreamRestoreState(); + } catch (IOException e) { + throwException(e); + } + return true; + } + + public boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, + int sx2, int sy2, Color bgcolor, ImageObserver observer) { + try { + contentStreamSaveState(); + int width = dx2 - dx1; + int height = dy2 - dy1; + + /* + * Set the clipping + */ + walkShape(new Rectangle2D.Double(dx1, dy1, width, height)); + contentStream.clip(); + + /* + * Maybe fill the background color + */ + if (bgcolor != null) { + contentStream.setNonStrokingColor(colorMapper.mapColor(contentStream, bgcolor)); + walkShape(new Rectangle(dx1, dy1, width, height)); + contentStream.fill(); + } + + /* + * Build the transform for the image + */ + AffineTransform tf = new AffineTransform(); + tf.translate(dx1, dy1); + float imgWidth = img.getWidth(observer); + float imgHeight = img.getHeight(observer); + tf.scale((float) width / imgWidth, (float) height / imgHeight); + tf.translate(-sx1, -sy1); + tf.scale((sx2 - sx1) / imgWidth, (sy2 - sy1) / imgHeight); + + drawImage(img, tf, observer); + contentStreamRestoreState(); + return true; + } catch (IOException e) { + throwException(e); + return false; + } + } + + private void drawStringUsingShapes(AttributedCharacterIterator iterator, float x, float y) { + Stroke originalStroke = stroke; + Paint originalPaint = paint; + TextLayout textLayout = new TextLayout(iterator, getFontRenderContext()); + textLayout.draw(this, x, y); + paint = originalPaint; + stroke = originalStroke; + } + + public void drawString(AttributedCharacterIterator iterator, float x, float y) { + /* + * Don't try to draw with no paint, just ignore that. + */ + if (paint == null) + return; + + try { + contentStreamSaveState(); + /* + * If we can draw the text using fonts, we do this + */ + if (fontTextDrawer + .canDrawText((AttributedCharacterIterator) iterator.clone(), fontDrawerEnv)) { + drawStringUsingText(iterator, x, y); + } else { + /* + * Otherwise we fall back to draw using shapes. This works always + */ + drawStringUsingShapes(iterator, x, y); + } + contentStreamRestoreState(); + } catch (IOException e) { + throwException(e); + } catch (FontFormatException e) { + throwException(e); + } + } + + private void drawStringUsingText(AttributedCharacterIterator iterator, float x, float y) + throws IOException, FontFormatException { + contentStreamSaveState(); + + AffineTransform tf = new AffineTransform(baseTransform); + tf.concatenate(transform); + tf.translate(x, y); + contentStream.transform(new Matrix(tf)); + + fontTextDrawer.drawText(iterator, fontDrawerEnv); + + contentStreamRestoreState(); + } + + private void contentStreamSaveState() throws IOException { + saveCounter++; + contentStream.saveGraphicsState(); + } + + private void contentStreamRestoreState() throws IOException { + if (saveCounter == 0) + throw new IllegalStateException( + "Internal save/restore state error. Should never happen."); + saveCounter--; + contentStream.restoreGraphicsState(); + } + + private final IFontTextDrawerEnv fontDrawerEnv = new IFontTextDrawerEnv() { + @Override + public PDDocument getDocument() { + return document; + } + + @Override + public PDPageContentStream getContentStream() { + return contentStream; + } + + @Override + public Font getFont() { + return font; + } + + @Override + public Paint getPaint() { + return paint; + } + + @Override + public void applyPaint(Paint paint, Shape shapeToDraw) throws IOException { + PDShading pdShading = PdfBoxGraphics2D.this.applyPaint(paint, shapeToDraw); + if (pdShading != null) + applyShadingAsColor(pdShading); + } + + @Override + public FontRenderContext getFontRenderContext() { + return PdfBoxGraphics2D.this.getFontRenderContext(); + } + + @Override + public PDRectangle getGraphicsBBox() { + return bbox; + } + + @Override + public PDResources getResources() { + return xFormObject.getResources(); + } + + @Override + public Graphics2D getCalculationGraphics() { + return calcGfx; + } + }; + + public void drawGlyphVector(GlyphVector g, float x, float y) { + checkNoCopyActive(); + AffineTransform transformOrig = (AffineTransform) transform.clone(); + transform.translate(x, y); + fill(g.getOutline()); + transform = transformOrig; + } + + public void fill(Shape s) { + checkNoCopyActive(); + + /* + * Don't try to draw with no paint, just ignore that. + */ + if (paint == null) + return; + + try { + contentStreamSaveState(); + + Shape shapeToFill = drawControl.transformShapeBeforeFill(s, drawControlEnv); + + if (shapeToFill != null) { + boolean useEvenOdd = walkShape(shapeToFill); + PDShading shading = applyPaint(shapeToFill); + if (shading != null) { + /* + * NB: the shading fill doesn't work with shapes with zero or negative + * dimensions (width and/or height): in these cases a normal fill is used + */ + Rectangle2D r2d = s.getBounds2D(); + if ((r2d.getWidth() <= 0) || (r2d.getHeight() <= 0)) { + /* + * But we apply the shading as color, we usually want to avoid that because it + * creates another nested XForm for that ... + */ + applyShadingAsColor(shading); + fill(useEvenOdd); + } else { + internalClip(useEvenOdd); + contentStream.shadingFill(shading); + } + } else { + fill(useEvenOdd); + } + hasPathOnStream = false; + } + + drawControl.afterShapeFill(s, drawControlEnv); + + contentStreamRestoreState(); + } catch (IOException e) { + throwException(e); + } + } + + private void fill(boolean useEvenOdd) throws IOException { + if (useEvenOdd) + contentStream.fillEvenOdd(); + else + contentStream.fill(); + } + + private void applyShadingAsColor(PDShading shading) throws IOException { + /* + * If the paint has a shading we must create a tiling pattern and set that as + * stroke color... + */ + PDTilingPattern pattern = new PDTilingPattern(); + pattern.setPaintType(PDTilingPattern.PAINT_COLORED); + pattern.setTilingType(PDTilingPattern.TILING_CONSTANT_SPACING_FASTER_TILING); + PDRectangle anchorRect = bbox; + pattern.setBBox(anchorRect); + pattern.setXStep(anchorRect.getWidth()); + pattern.setYStep(anchorRect.getHeight()); + + PDAppearanceStream appearance = new PDAppearanceStream(this.document); + appearance.setResources(pattern.getResources()); + appearance.setBBox(pattern.getBBox()); + + PDPageContentStream imageContentStream = new PDPageContentStream(document, appearance, + ((COSStream) pattern.getCOSObject()).createOutputStream()); + imageContentStream.addRect(0, 0, anchorRect.getWidth(), anchorRect.getHeight()); + imageContentStream.clip(); + imageContentStream.shadingFill(shading); + imageContentStream.close(); + + PDColorSpace patternCS1 = new PDPattern(null); + COSName tilingPatternName = xFormObject.getResources().add(pattern); + PDColor patternColor = new PDColor(tilingPatternName, patternCS1); + + contentStream.setNonStrokingColor(patternColor); + contentStream.setStrokingColor(patternColor); + } + + private PDShading applyPaint(Shape shapeToDraw) throws IOException { + return applyPaint(paint, shapeToDraw); + } + + private final PaintEnvImpl paintEnv = new PaintEnvImpl(); + + private PDShading applyPaint(Paint paintToApply, Shape shapeToDraw) throws IOException { + AffineTransform tf = new AffineTransform(baseTransform); + tf.concatenate(transform); + paintEnv.shapeToDraw = shapeToDraw; + return paintApplier.applyPaint(paintToApply, contentStream, tf, paintEnv); + } + + public boolean hit(Rectangle rect, Shape s, boolean onStroke) { + return false; + } + + public GraphicsConfiguration getDeviceConfiguration() { + return null; + } + + public void setComposite(Composite comp) { + composite = comp; + } + + public void setPaint(Paint paint) { + this.paint = paint; + } + + public void setStroke(Stroke stroke) { + this.stroke = stroke; + } + + private Map renderingHints = new HashMap(); + + public void setRenderingHint(Key hintKey, Object hintValue) { + renderingHints.put(hintKey, hintValue); + } + + public Object getRenderingHint(Key hintKey) { + return renderingHints.get(hintKey); + } + + public void setRenderingHints(Map hints) { + hints.clear(); + addRenderingHints(hints); + } + + @SuppressWarnings("unchecked") + public void addRenderingHints(Map hints) { + renderingHints.putAll((Map) hints); + + } + + public RenderingHints getRenderingHints() { + return new RenderingHints(renderingHints); + } + + /** + * Creates a copy of this graphics object. Please call {@link #dispose()} always + * on the copy after you have finished drawing with it.
+ *
+ * Never draw both in this copy and its parent graphics at the same time, as + * they all write to the same content stream. This will create a broken PDF + * content stream. You should get an {@link IllegalStateException} if + * you do so, but better just don't try.
+ *
+ * The copy allows you to have different transforms, paints, etc. than the + * parent graphics context without affecting the parent. You may also call + * create() on a copy, but always remember to call {@link #dispose()} in reverse + * order. + * + * @return a copy of this Graphics. + */ + public PdfBoxGraphics2D create() { + try { + return new PdfBoxGraphics2D(this); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + public PdfBoxGraphics2D create(int x, int y, int width, int height) { + return (PdfBoxGraphics2D) super.create(x, y, width, height); + } + + public void translate(int x, int y) { + transform.translate(x, y); + } + + public Color getColor() { + if (paint instanceof Color) + return (Color) paint; + return null; + } + + public void setColor(Color color) { + this.paint = color; + } + + public void setPaintMode() { + xorColor = null; + } + + /** + * XOR Mode is currently not implemented as it's not possible in PDF. This mode + * is ignored. + * + * @param c1 the XORMode Color + */ + public void setXORMode(Color c1) { + xorColor = c1; + } + + public Font getFont() { + return font; + } + + public void setFont(Font font) { + this.font = font; + } + + public FontMetrics getFontMetrics(Font f) { + try { + return fontTextDrawer.getFontMetrics(f, fontDrawerEnv); + } catch (IOException e) { + throw new RuntimeException(e); + } catch (FontFormatException e) { + throw new RuntimeException(e); + } + } + + public Rectangle getClipBounds() { + Shape clip = getClip(); + if (clip != null) + return clip.getBounds(); + return null; + } + + public void clipRect(int x, int y, int width, int height) { + Rectangle2D rect = new Rectangle2D.Double(x, y, width, height); + clip(rect); + } + + public void setClip(int x, int y, int width, int height) { + setClip(new Rectangle(x, y, width, height)); + } + + public Shape getClip() { + try { + return transform.createInverse().createTransformedShape(clipShape); + } catch (NoninvertibleTransformException e) { + return null; + } + } + + public void setClip(Shape clip) { + checkNoCopyActive(); + this.clipShape = transform.createTransformedShape(clip); + /* + * Clip on the content stream + */ + try { + contentStreamRestoreState(); + contentStreamSaveState(); + /* + * clip can be null, only set a clipping if not null + */ + if (clip != null) { + internalClip(walkShape(clip)); + } + } catch (IOException e) { + throwException(e); + } + } + + /** + * Perform a clip, but only if we really have an active clipping path + * + * @param useEvenOdd true when we should use the evenOdd rule. + */ + void internalClip(boolean useEvenOdd) throws IOException { + if (hasPathOnStream) { + if (useEvenOdd) + contentStream.clipEvenOdd(); + else + contentStream.clip(); + hasPathOnStream = false; + } + } + + /** + * Float#isFinite() is JDK 8+. We just copied the trivial implementation here. + * When we require JDK 8+ we can just drop this method and replace it bei + * Float#isFinite() + */ + private static boolean isFinite(float f) { + return Math.abs(f) <= Float.MAX_VALUE; + } + + /** + * @return true when all required values are finite + */ + private static boolean isFinite(float[] coords, int count) { + for (int i = 0; i < count; i++) + if (!isFinite(coords[i])) + return false; + return true; + } + + /** + * Do we currently have an active path on the content stream, which has not been + * closed? + *

+ * We need this flag to avoid to clip twice if both the plaint applyer needs to + * clip and we have some clipping. If at the end we try to clip with an empty + * path, then Acrobat Reader does not like that and draws nothing. + */ + private boolean hasPathOnStream = false; + + /** + * Set an internal flag that some path - which may be added from the paint + * applyer to the content stream or by walkShape() - is on the content stream. + * We can then safely clip() if there is a path on the content stream. + */ + void markPathIsOnStream() { + hasPathOnStream = true; + } + + /** + * Walk the path and return true if we need to use the even odd winding rule. + * + * @return true if we need to use the even odd winding rule + */ + private boolean walkShape(Shape clip) throws IOException { + checkNoCopyActive(); + + AffineTransform tf = new AffineTransform(baseTransform); + tf.concatenate(transform); + PathIterator pi = clip.getPathIterator(tf); + float[] coords = new float[6]; + while (!pi.isDone()) { + int segment = pi.currentSegment(coords); + switch (segment) { + case PathIterator.SEG_MOVETO: + if (isFinite(coords, 2)) + contentStream.moveTo(coords[0], coords[1]); + break; + case PathIterator.SEG_LINETO: + if (isFinite(coords, 2)) + contentStream.lineTo(coords[0], coords[1]); + break; + case PathIterator.SEG_QUADTO: + if (isFinite(coords, 4)) + contentStream.curveTo1(coords[0], coords[1], coords[2], coords[3]); + break; + case PathIterator.SEG_CUBICTO: + if (isFinite(coords, 6)) + contentStream.curveTo(coords[0], coords[1], coords[2], coords[3], coords[4], + coords[5]); + break; + case PathIterator.SEG_CLOSE: + contentStream.closePath(); + break; + } + pi.next(); + } + markPathIsOnStream(); + return pi.getWindingRule() == PathIterator.WIND_EVEN_ODD; + } + + private void checkNoCopyActive() { + /* + * As long as a copy is in use you are not allowed to do anything here + */ + if (copyList.size() > 0) + throw new IllegalStateException( + "Don't use the main context as long as a copy is active! Child context is missing a .dispose() call. \n" + + gatherDebugCopyInfo(this)); + } + + private static String gatherDebugCopyInfo(PdfBoxGraphics2D gfx) { + StringBuilder sb = new StringBuilder(); + if (!gfx.copyList.isEmpty()) { + for (CopyInfo copyInfo : gfx.copyList) { + sb.append("# Dangling Child").append(copyInfo.toString()).append("\n"); + } + } + + while (gfx != null) { + if (gfx.copyList.isEmpty()) { + sb.append("* Last Child\n"); + } else { + sb.append("- Parent with ").append(gfx.copyList.size()).append(" childs.\n"); + } + if (gfx.copyInfo == null) + break; + gfx = gfx.copyInfo.sourceGfx; + } + return sb.toString(); + } + + private void throwException(Exception e) { + throw new RuntimeException(e); + } + + public void copyArea(int x, int y, int width, int height, int dx, int dy) { + /* + * Sorry, cant do that :( + */ + throw new IllegalStateException("copyArea() not possible!"); + } + + public void drawLine(int x1, int y1, int x2, int y2) { + draw(new Line2D.Double(x1, y1, x2, y2)); + } + + public void fillRect(int x, int y, int width, int height) { + fill(new Rectangle(x, y, width, height)); + } + + public void clearRect(int x, int y, int width, int height) { + Paint p = paint; + paint = backgroundColor; + fillRect(x, y, width, height); + paint = p; + } + + public void drawRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + draw(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight)); + } + + public void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight) { + fill(new RoundRectangle2D.Double(x, y, width, height, arcWidth, arcHeight)); + } + + public void drawOval(int x, int y, int width, int height) { + draw(new Ellipse2D.Double(x, y, width, height)); + } + + public void fillOval(int x, int y, int width, int height) { + fill(new Ellipse2D.Double(x, y, width, height)); + } + + public void drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + draw(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.OPEN)); + } + + public void fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) { + fill(new Arc2D.Double(x, y, width, height, startAngle, arcAngle, Arc2D.PIE)); + } + + public void drawPolyline(int[] xPoints, int[] yPoints, int nPoints) { + Path2D.Double path = new Path2D.Double(); + path.moveTo(xPoints[0], yPoints[0]); + for (int i = 1; i < nPoints; i++) + path.lineTo(xPoints[i], yPoints[i]); + draw(path); + } + + public void drawPolygon(int[] xPoints, int[] yPoints, int nPoints) { + draw(new Polygon(xPoints, yPoints, nPoints)); + } + + public void fillPolygon(int[] xPoints, int[] yPoints, int nPoints) { + fill(new Polygon(xPoints, yPoints, nPoints)); + } + + public void translate(double tx, double ty) { + checkNoCopyActive(); + transform.translate(tx, ty); + } + + public void rotate(double theta) { + checkNoCopyActive(); + transform.rotate(theta); + } + + public void rotate(double theta, double x, double y) { + checkNoCopyActive(); + transform.rotate(theta, x, y); + } + + public void scale(double sx, double sy) { + checkNoCopyActive(); + transform.scale(sx, sy); + } + + public void shear(double shx, double shy) { + checkNoCopyActive(); + transform.shear(shx, shy); + } + + public void transform(AffineTransform Tx) { + checkNoCopyActive(); + transform.concatenate(Tx); + } + + public void setTransform(AffineTransform Tx) { + checkNoCopyActive(); + transform = new AffineTransform(); + transform.concatenate(Tx); + } + + public AffineTransform getTransform() { + return (AffineTransform) transform.clone(); + } + + public Paint getPaint() { + return paint; + } + + public Composite getComposite() { + return composite; + } + + public void setBackground(Color color) { + backgroundColor = color; + } + + public Color getBackground() { + return backgroundColor; + } + + public Stroke getStroke() { + return stroke; + } + + public void clip(Shape shape) { + Shape clip = getClip(); + if (clip == null) + setClip(shape); + else { + Area area = new Area(clip); + area.intersect(new Area(shape)); + setClip(area); + } + } + + public FontRenderContext getFontRenderContext() { + calcGfx.addRenderingHints(renderingHints); + return calcGfx.getFontRenderContext(); + } + + private class PaintEnvImpl implements IPaintEnv { + public Shape shapeToDraw; + + @Override + public Shape getShapeToDraw() { + return shapeToDraw; + } + + @Override + public ColorMapper getColorMapper() { + return colorMapper; + } + + @Override + public ImageEncoder getImageEncoder() { + return imageEncoder; + } + + @Override + public PDDocument getDocument() { + return document; + } + + @Override + public PDResources getResources() { + return xFormObject.getResources(); + } + + @Override + public Composite getComposite() { + return PdfBoxGraphics2D.this.getComposite(); + } + + @Override + public PdfBoxGraphics2D getGraphics2D() { + return PdfBoxGraphics2D.this; + } + + @Override + public Color getXORMode() { + return xorColor; + } + } +} diff --git a/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/RGBtoCMYKColorMapper.java b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/RGBtoCMYKColorMapper.java new file mode 100644 index 0000000..938bdd5 --- /dev/null +++ b/graphics2d-pdfbox/src/main/java/org/xbib/graphics/graphics2d/pdfbox/RGBtoCMYKColorMapper.java @@ -0,0 +1,63 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.cos.COSName; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDICCBased; + +import java.awt.Color; +import java.awt.color.ICC_ColorSpace; +import java.awt.color.ICC_Profile; +import java.io.IOException; +import java.io.OutputStream; + +/* + Usage: + + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(this.doc, (int)(width), (int)(height)); + PdfBoxGraphics2DColorMapper colorMapper = new RGBtoCMYKColorMapper(icc_profile); + pdfBoxGraphics2D.setColorMapper(colorMapper); + + Where icc_profile is an instance of java.awt.color.ICC_Profile that supports a CMYK + colorspace. For testing purposes, we're using ISOcoated_v2_300_bas.icc which ships + with PDFBox. + */ +public class RGBtoCMYKColorMapper extends DefaultColorMapper { + ICC_ColorSpace icc_colorspace; + PDICCBased pdProfile; + + public RGBtoCMYKColorMapper(ICC_Profile icc_profile, PDDocument document) throws IOException { + icc_colorspace = new ICC_ColorSpace(icc_profile); + this.pdProfile = new PDICCBased(document); + OutputStream outputStream = pdProfile.getPDStream().createOutputStream(COSName.FLATE_DECODE); + outputStream.write(icc_profile.getData()); + outputStream.close(); + pdProfile.getPDStream().getCOSObject().setInt(COSName.N, 4); + pdProfile.getPDStream().getCOSObject().setItem(COSName.ALTERNATE, COSName.DEVICECMYK); + } + + public PDColor mapColor(PDPageContentStream contentStream, Color rgbColor) { + int r = rgbColor.getRed(); + int g = rgbColor.getGreen(); + int b = rgbColor.getBlue(); + int[] rgbInts = {r, g, b}; + float[] rgbFoats = rgbIntToFloat(rgbInts); + float[] cmykFloats = icc_colorspace.fromRGB(rgbFoats); + + PDColor cmykColor = new PDColor(cmykFloats, pdProfile); + return cmykColor; + } + + public static float[] rgbIntToFloat(int[] rgbInts) { + // the input ints are in the range 0 to 255 + // the output floats need to be in the range 0.0 to 1.0 + float red = (float) rgbInts[0] / 255.0F; + float green = (float) rgbInts[1] / 255.0F; + float blue = (float) rgbInts[2] / 255.0F; + float[] rgbFloats = new float[]{red, green, blue}; + return rgbFloats; + } + +} + diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/DanglingGfxCaseTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/DanglingGfxCaseTest.java new file mode 100644 index 0000000..60e98dc --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/DanglingGfxCaseTest.java @@ -0,0 +1,76 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.util.Matrix; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.awt.Color; +import java.io.File; +import java.io.IOException; + +public class DanglingGfxCaseTest { + + @Test + public void testDanglingGfx() throws IOException { + + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + PDPageContentStream contentStream = new PDPageContentStream(document, page); + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(document, 400, 400); + + PdfBoxGraphics2D child = pdfBoxGraphics2D.create(10, 10, 40, 40); + child.setColor(Color.RED); + child.fillRect(0, 0, 100, 100); + + PdfBoxGraphics2D child2 = child.create(20, 20, 10, 10); + child2.setColor(Color.GREEN); + child2.drawOval(0, 0, 5, 5); + + child.create(); + + pdfBoxGraphics2D.disposeDanglingChildGraphics(); + pdfBoxGraphics2D.dispose(); + + PDFormXObject appearanceStream = pdfBoxGraphics2D.getXFormObject(); + Matrix matrix = new Matrix(); + matrix.translate(0, 20); + contentStream.transform(matrix); + contentStream.drawForm(appearanceStream); + contentStream.close(); + + File file = new File("build/test/dangling_test.pdf"); + file.getParentFile().mkdirs(); + document.save(file); + document.close(); + } + + @Test + public void testDanglingDisposeException() { + Assertions.assertThrows(IllegalStateException.class, () -> { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(document, 400, 400); + pdfBoxGraphics2D.create(); + pdfBoxGraphics2D.dispose(); + }); + } + + @Test + public void testDanglingDisposeException2() { + Assertions.assertThrows(IllegalStateException.class, () -> { + PDDocument document = new PDDocument(); + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(document, 400, 400); + pdfBoxGraphics2D.create().disposeDanglingChildGraphics(); + }); + } +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/FontTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/FontTest.java new file mode 100644 index 0000000..b61e6eb --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/FontTest.java @@ -0,0 +1,28 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.junit.jupiter.api.Test; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.Graphics2D; +import java.io.IOException; + +public class FontTest extends PdfBoxGraphics2DTestBase { + + @Test + public void testAntonioFont() throws IOException, FontFormatException { + final Font antonioRegular = Font + .createFont(Font.TRUETYPE_FONT, + PdfBoxGraphics2dTest.class.getResourceAsStream("antonio/Antonio-Regular.ttf")) + .deriveFont(15f); + exportGraphic("fonts", "antonio", new GraphicsExporter() { + @Override + public void draw(Graphics2D gfx) throws IOException, FontFormatException { + gfx.setColor(Color.BLACK); + gfx.setFont(antonioRegular); + gfx.drawString("Für älter österlich, Umlauts are not always fun.", 10, 50); + } + }); + } +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/FontWidthDiscrepancyTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/FontWidthDiscrepancyTest.java new file mode 100644 index 0000000..4ef3a88 --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/FontWidthDiscrepancyTest.java @@ -0,0 +1,63 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDType0Font; +import org.junit.jupiter.api.Test; + +import java.awt.Color; +import java.awt.Font; +import java.awt.FontFormatException; +import java.awt.FontMetrics; +import java.awt.Graphics2D; +import java.io.IOException; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class FontWidthDiscrepancyTest extends PdfBoxGraphics2DTestBase { + + @Test + public void testAntonioFontWidth() throws IOException, FontFormatException { + + final String testString = "MMMMMMMMMMMMMMMMMMMMMM"; + final float fontSize = 20f; + final Font antonioRegular = Font.createFont(Font.TRUETYPE_FONT, + PdfBoxGraphics2dTest.class.getResourceAsStream("antonio/Antonio-Regular.ttf")) + .deriveFont(fontSize); + + final PDDocument doc = new PDDocument(); + final PDFont pdFont = PDType0Font.load(doc, + PdfBoxGraphics2dTest.class.getResourceAsStream("antonio/Antonio-Regular.ttf")); + + final Graphics2D gfx = new PdfBoxGraphics2D(doc, 400, 400); + + final float pdfWidth = pdFont.getStringWidth(testString) / 1000 * fontSize; + final int gfxWidth = gfx.getFontMetrics(antonioRegular).stringWidth(testString); + gfx.dispose(); + doc.close(); + + exportGraphic("fontWidthDiscrepancy", "antonio-m", new GraphicsExporter() { + @Override + public void draw(Graphics2D gfx) throws IOException, FontFormatException { + gfx.setFont(antonioRegular); + gfx.setColor(Color.GREEN); + gfx.drawString(testString, 10, 10); + gfx.setColor(Color.RED); + gfx.drawLine(10, 1, (int) (10 + pdfWidth), 1); + gfx.setColor(Color.BLUE); + gfx.drawLine(10, 15, 10 + gfxWidth, 15); + + gfx.setColor(Color.magenta); + FontMetrics fontMetrics = gfx.getFontMetrics(); + int currentMeasurement = fontMetrics.stringWidth(testString); + gfx.drawLine(10, 25, 10 + currentMeasurement, 25); + + gfx.drawLine(10, 5, 10 + fontMetrics.charWidth('M'), 5); + + assertNotNull(fontMetrics.getWidths()); + } + }); + + } + +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/MultiPageTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/MultiPageTest.java new file mode 100644 index 0000000..6ac7c4b --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/MultiPageTest.java @@ -0,0 +1,408 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.util.Matrix; +import org.jfree.chart.ChartFactory; +//import org.jfree.chart.ChartUtilities; +import org.jfree.chart.ChartUtils; +import org.jfree.chart.JFreeChart; +import org.jfree.chart.axis.NumberAxis; +import org.jfree.chart.labels.StandardCategoryToolTipGenerator; +import org.jfree.chart.plot.*; +import org.jfree.chart.renderer.xy.XYLineAndShapeRenderer; +import org.jfree.chart.title.LegendTitle; +import org.jfree.chart.title.TextTitle; +import org.jfree.chart.ui.RectangleEdge; +import org.jfree.chart.util.TableOrder; +import org.jfree.data.category.CategoryDataset; +import org.jfree.data.category.DefaultCategoryDataset; +import org.jfree.data.category.IntervalCategoryDataset; +import org.jfree.data.gantt.Task; +import org.jfree.data.gantt.TaskSeries; +import org.jfree.data.gantt.TaskSeriesCollection; +import org.jfree.data.time.SimpleTimePeriod; +import org.jfree.data.xy.XYDataset; +import org.jfree.data.xy.XYSeries; +import org.jfree.data.xy.XYSeriesCollection; +//import org.jfree.ui.RectangleEdge; +//import org.jfree.util.TableOrder; +import org.junit.jupiter.api.Test; + +import java.awt.Color; +import java.awt.Font; +import java.awt.Rectangle; +import java.io.File; +import java.io.IOException; +import java.util.Calendar; +import java.util.Date; + +public class MultiPageTest { + + @Test + public void testMultiPageJFreeChart() throws IOException { + File parentDir = new File("build/test/multipage"); + // noinspection ResultOfMethodCallIgnored + parentDir.mkdirs(); + File targetPDF = new File(parentDir, "multipage.pdf"); + PDDocument document = new PDDocument(); + for (int i = 0; i < 6; i++) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + PDPageContentStream contentStream = new PDPageContentStream(document, page); + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(document, 800, 400); + drawOnGraphics(pdfBoxGraphics2D, i); + pdfBoxGraphics2D.dispose(); + + PDFormXObject appearanceStream = pdfBoxGraphics2D.getXFormObject(); + Matrix matrix = new Matrix(); + matrix.translate(0, 30); + matrix.scale(0.7f, 1f); + + contentStream.saveGraphicsState(); + contentStream.transform(matrix); + contentStream.drawForm(appearanceStream); + contentStream.restoreGraphicsState(); + + contentStream.close(); + } + document.save(targetPDF); + document.close(); + } + + private void drawOnGraphics(PdfBoxGraphics2D gfx, int i) { + Rectangle rectangle = new Rectangle(800, 400); + switch (i) { + case 0: + case 3: { + final XYDataset dataset = createDatasetXY(); + final JFreeChart chart = createChartXY(dataset); + chart.draw(gfx, rectangle); + break; + } + case 1: { + final IntervalCategoryDataset dataset = createDatasetGantt(); + final JFreeChart chart = createChartGantt(dataset); + chart.draw(gfx, rectangle); + break; + } + case 2: { + final CategoryDataset dataset = createDatasetCategory(); + final JFreeChart chart = createChartCategory(dataset); + chart.draw(gfx, rectangle); + break; + } + case 4: { + final CategoryDataset dataset = createDatasetCategory(); + final JFreeChart chart = createSpiderChart(dataset); + chart.draw(gfx, rectangle); + break; + } + case 5: { + final DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + dataset.addValue(0.0, "Row 0", "Column 0"); + dataset.addValue(0.0, "Row 0", "Column 1"); + dataset.addValue(0.0, "Row 0", "Column 2"); + dataset.addValue(0.0, "Row 0", "Column 3"); + dataset.addValue(0.0, "Row 0", "Column 4"); + final JFreeChart chart = createSpiderChart(dataset); + chart.setTitle("Invalid Spider Chart"); + chart.draw(gfx, rectangle); + break; + } + } + } + + /** + * Creates a sample dataset. + * + * @return a sample dataset. + */ + private XYDataset createDatasetXY() { + + final XYSeries series1 = new XYSeries("First"); + series1.add(1.0, 1.0); + series1.add(2.0, 4.0); + series1.add(3.0, 3.0); + series1.add(4.0, 5.0); + series1.add(5.0, 5.0); + series1.add(6.0, 7.0); + series1.add(7.0, 7.0); + series1.add(8.0, 8.0); + + final XYSeries series2 = new XYSeries("Second"); + series2.add(1.0, 5.0); + series2.add(2.0, 7.0); + series2.add(3.0, 6.0); + series2.add(4.0, 8.0); + series2.add(5.0, 4.0); + series2.add(6.0, 4.0); + series2.add(7.0, 2.0); + series2.add(8.0, 1.0); + + final XYSeries series3 = new XYSeries("Third"); + series3.add(3.0, 4.0); + series3.add(4.0, 3.0); + series3.add(5.0, 2.0); + series3.add(6.0, 3.0); + series3.add(7.0, 6.0); + series3.add(8.0, 3.0); + series3.add(9.0, 4.0); + series3.add(10.0, 3.0); + + final XYSeriesCollection dataset = new XYSeriesCollection(); + dataset.addSeries(series1); + dataset.addSeries(series2); + dataset.addSeries(series3); + + return dataset; + + } + + /** + * Creates a chart. + * + * @param dataset the data for the chart. + * @return a chart. + */ + private JFreeChart createChartXY(final XYDataset dataset) { + + // create the chart... + final JFreeChart chart = ChartFactory.createXYLineChart("Line Chart Demo 6", // chart + // title + "X", // x axis label + "Y", // y axis label + dataset, // data + PlotOrientation.VERTICAL, true, // include legend + true, // tooltips + false // urls + ); + + // NOW DO SOME OPTIONAL CUSTOMISATION OF THE CHART... + chart.setBackgroundPaint(Color.white); + + // final StandardLegend legend = (StandardLegend) chart.getLegend(); + // legend.setDisplaySeriesShapes(true); + + // get a reference to the plot for further customisation... + final XYPlot plot = chart.getXYPlot(); + plot.setBackgroundPaint(Color.lightGray); + // plot.setAxisOffset(new Spacer(Spacer.ABSOLUTE, 5.0, 5.0, 5.0, 5.0)); + plot.setDomainGridlinePaint(Color.white); + plot.setRangeGridlinePaint(Color.white); + + final XYLineAndShapeRenderer renderer = new XYLineAndShapeRenderer(); + renderer.setSeriesLinesVisible(0, false); + renderer.setSeriesShapesVisible(1, false); + plot.setRenderer(renderer); + + // change the auto tick unit selection to integer units only... + final NumberAxis rangeAxis = (NumberAxis) plot.getRangeAxis(); + rangeAxis.setStandardTickUnits(NumberAxis.createIntegerTickUnits()); + // OPTIONAL CUSTOMISATION COMPLETED. + + return chart; + + } + + /** + * Creates a sample dataset for a Gantt chart. + * + * @return The dataset. + */ + private static IntervalCategoryDataset createDatasetGantt() { + + final TaskSeries s1 = new TaskSeries("Scheduled"); + s1.add(new Task("Write Proposal", + new SimpleTimePeriod(date(1, Calendar.APRIL, 2001), date(5, Calendar.APRIL, 2001)))); + s1.add(new Task("Obtain Approval", + new SimpleTimePeriod(date(9, Calendar.APRIL, 2001), date(9, Calendar.APRIL, 2001)))); + s1.add(new Task("Requirements Analysis", + new SimpleTimePeriod(date(10, Calendar.APRIL, 2001), date(5, Calendar.MAY, 2001)))); + s1.add(new Task("Design Phase", + new SimpleTimePeriod(date(6, Calendar.MAY, 2001), date(30, Calendar.MAY, 2001)))); + s1.add(new Task("Design Signoff", + new SimpleTimePeriod(date(2, Calendar.JUNE, 2001), date(2, Calendar.JUNE, 2001)))); + s1.add(new Task("Alpha Implementation", + new SimpleTimePeriod(date(3, Calendar.JUNE, 2001), date(31, Calendar.JULY, 2001)))); + s1.add(new Task("Design Review", + new SimpleTimePeriod(date(1, Calendar.AUGUST, 2001), date(8, Calendar.AUGUST, 2001)))); + s1.add(new Task("Revised Design Signoff", + new SimpleTimePeriod(date(10, Calendar.AUGUST, 2001), date(10, Calendar.AUGUST, 2001)))); + s1.add(new Task("Beta Implementation", + new SimpleTimePeriod(date(12, Calendar.AUGUST, 2001), date(12, Calendar.SEPTEMBER, 2001)))); + s1.add(new Task("Testing", + new SimpleTimePeriod(date(13, Calendar.SEPTEMBER, 2001), date(31, Calendar.OCTOBER, 2001)))); + s1.add(new Task("Final Implementation", + new SimpleTimePeriod(date(1, Calendar.NOVEMBER, 2001), date(15, Calendar.NOVEMBER, 2001)))); + s1.add(new Task("Signoff", + new SimpleTimePeriod(date(28, Calendar.NOVEMBER, 2001), date(30, Calendar.NOVEMBER, 2001)))); + + final TaskSeries s2 = new TaskSeries("Actual"); + s2.add(new Task("Write Proposal", + new SimpleTimePeriod(date(1, Calendar.APRIL, 2001), date(5, Calendar.APRIL, 2001)))); + s2.add(new Task("Obtain Approval", + new SimpleTimePeriod(date(9, Calendar.APRIL, 2001), date(9, Calendar.APRIL, 2001)))); + s2.add(new Task("Requirements Analysis", + new SimpleTimePeriod(date(10, Calendar.APRIL, 2001), date(15, Calendar.MAY, 2001)))); + s2.add(new Task("Design Phase", + new SimpleTimePeriod(date(15, Calendar.MAY, 2001), date(17, Calendar.JUNE, 2001)))); + s2.add(new Task("Design Signoff", + new SimpleTimePeriod(date(30, Calendar.JUNE, 2001), date(30, Calendar.JUNE, 2001)))); + s2.add(new Task("Alpha Implementation", + new SimpleTimePeriod(date(1, Calendar.JULY, 2001), date(12, Calendar.SEPTEMBER, 2001)))); + s2.add(new Task("Design Review", + new SimpleTimePeriod(date(12, Calendar.SEPTEMBER, 2001), date(22, Calendar.SEPTEMBER, 2001)))); + s2.add(new Task("Revised Design Signoff", + new SimpleTimePeriod(date(25, Calendar.SEPTEMBER, 2001), date(27, Calendar.SEPTEMBER, 2001)))); + s2.add(new Task("Beta Implementation", + new SimpleTimePeriod(date(27, Calendar.SEPTEMBER, 2001), date(30, Calendar.OCTOBER, 2001)))); + s2.add(new Task("Testing", + new SimpleTimePeriod(date(31, Calendar.OCTOBER, 2001), date(17, Calendar.NOVEMBER, 2001)))); + s2.add(new Task("Final Implementation", + new SimpleTimePeriod(date(18, Calendar.NOVEMBER, 2001), date(5, Calendar.DECEMBER, 2001)))); + s2.add(new Task("Signoff", + new SimpleTimePeriod(date(10, Calendar.DECEMBER, 2001), date(11, Calendar.DECEMBER, 2001)))); + + final TaskSeriesCollection collection = new TaskSeriesCollection(); + collection.add(s1); + collection.add(s2); + + return collection; + } + + /** + * Utility method for creating Date objects. + * + * @param day the date. + * @param month the month. + * @param year the year. + * @return a date. + */ + private static Date date(final int day, final int month, @SuppressWarnings("SameParameterValue") final int year) { + final Calendar calendar = Calendar.getInstance(); + calendar.set(year, month, day); + return calendar.getTime(); + + } + + /** + * Creates a chart. + * + * @param dataset the dataset. + * @return The chart. + */ + private JFreeChart createChartGantt(final IntervalCategoryDataset dataset) { + return ChartFactory.createGanttChart("Gantt Chart Demo", // chart + // title + "Task", // domain axis label + "Date", // range axis label + dataset, // data + true, // include legend + true, // tooltips + false // urls + ); + } + + /** + * Creates a sample dataset. + * + * @return A sample dataset. + */ + private CategoryDataset createDatasetCategory() { + final DefaultCategoryDataset dataset = new DefaultCategoryDataset(); + dataset.addValue(5.6, "Row 0", "Column 0"); + dataset.addValue(3.2, "Row 0", "Column 1"); + dataset.addValue(1.8, "Row 0", "Column 2"); + dataset.addValue(0.2, "Row 0", "Column 3"); + dataset.addValue(4.1, "Row 0", "Column 4"); + + dataset.addValue(9.8, "Row 1", "Column 0"); + dataset.addValue(6.3, "Row 1", "Column 1"); + dataset.addValue(0.1, "Row 1", "Column 2"); + dataset.addValue(1.9, "Row 1", "Column 3"); + dataset.addValue(9.6, "Row 1", "Column 4"); + + dataset.addValue(7.0, "Row 2", "Column 0"); + dataset.addValue(5.2, "Row 2", "Column 1"); + dataset.addValue(2.8, "Row 2", "Column 2"); + dataset.addValue(8.8, "Row 2", "Column 3"); + dataset.addValue(7.2, "Row 2", "Column 4"); + + dataset.addValue(9.5, "Row 3", "Column 0"); + dataset.addValue(1.2, "Row 3", "Column 1"); + dataset.addValue(4.5, "Row 3", "Column 2"); + dataset.addValue(4.4, "Row 3", "Column 3"); + dataset.addValue(0.2, "Row 3", "Column 4"); + + dataset.addValue(3.5, "Row 4", "Column 0"); + dataset.addValue(6.7, "Row 4", "Column 1"); + dataset.addValue(9.0, "Row 4", "Column 2"); + dataset.addValue(1.0, "Row 4", "Column 3"); + dataset.addValue(5.2, "Row 4", "Column 4"); + + dataset.addValue(5.1, "Row 5", "Column 0"); + dataset.addValue(6.7, "Row 5", "Column 1"); + dataset.addValue(0.9, "Row 5", "Column 2"); + dataset.addValue(3.3, "Row 5", "Column 3"); + dataset.addValue(3.9, "Row 5", "Column 4"); + + dataset.addValue(5.6, "Row 6", "Column 0"); + dataset.addValue(5.6, "Row 6", "Column 1"); + dataset.addValue(5.6, "Row 6", "Column 2"); + dataset.addValue(5.6, "Row 6", "Column 3"); + dataset.addValue(5.6, "Row 6", "Column 4"); + + dataset.addValue(7.5, "Row 7", "Column 0"); + dataset.addValue(9.0, "Row 7", "Column 1"); + dataset.addValue(3.4, "Row 7", "Column 2"); + dataset.addValue(4.1, "Row 7", "Column 3"); + dataset.addValue(0.5, "Row 7", "Column 4"); + + return dataset; + } + + /** + * Creates a sample chart for the given dataset. + * + * @param dataset the dataset. + * @return A sample chart. + */ + private JFreeChart createChartCategory(final CategoryDataset dataset) { + final JFreeChart chart = ChartFactory.createMultiplePieChart3D("Multiple Pie Chart Demo 4", dataset, + TableOrder.BY_COLUMN, false, true, false); + chart.setBackgroundPaint(new Color(216, 255, 216)); + final MultiplePiePlot plot = (MultiplePiePlot) chart.getPlot(); + final JFreeChart subchart = plot.getPieChart(); + // final StandardLegend legend = new StandardLegend(); + // legend.setItemFont(new Font("SansSerif", Font.PLAIN, 8)); + // legend.setAnchor(Legend.SOUTH); + // subchart.setLegend(legend); + plot.setLimit(0.10); + final PiePlot p = (PiePlot) subchart.getPlot(); + // p.setLabelGenerator(new StandardPieItemLabelGenerator("{0}")); + p.setLabelFont(new Font("SansSerif", Font.PLAIN, 8)); + p.setInteriorGap(0.30); + + return chart; + } + + private static JFreeChart createSpiderChart(CategoryDataset dataset) { + SpiderWebPlot plot = new SpiderWebPlot(dataset); + plot.setStartAngle(54); + plot.setInteriorGap(0.40); + plot.setToolTipGenerator(new StandardCategoryToolTipGenerator()); + JFreeChart chart = new JFreeChart("Spider Web Chart Demo 1", TextTitle.DEFAULT_FONT, plot, false); + LegendTitle legend = new LegendTitle(plot); + legend.setPosition(RectangleEdge.BOTTOM); + chart.addSubtitle(legend); + ChartUtils.applyCurrentTheme(chart); + return chart; + + } + +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2DFontTextDrawerDefaultFontsTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2DFontTextDrawerDefaultFontsTest.java new file mode 100644 index 0000000..d1ab7b9 --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2DFontTextDrawerDefaultFontsTest.java @@ -0,0 +1,70 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.junit.jupiter.api.Test; + +import java.awt.Font; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class PdfBoxGraphics2DFontTextDrawerDefaultFontsTest { + + @Test + public void testFontStyleMatching() { + Font anyFont = Font.decode("Dialog"); + Font anyFontBold = anyFont.deriveFont(Font.BOLD); + Font anyFontItalic = anyFont.deriveFont(Font.ITALIC); + Font anyFontBoldItalic = anyFont.deriveFont(Font.BOLD | Font.ITALIC); + + assertEquals(PDType1Font.COURIER, DefaultFontTextDrawerDefaultFonts.chooseMatchingCourier(anyFont)); + assertEquals(PDType1Font.COURIER_BOLD, + DefaultFontTextDrawerDefaultFonts.chooseMatchingCourier(anyFontBold)); + assertEquals(PDType1Font.COURIER_OBLIQUE, + DefaultFontTextDrawerDefaultFonts.chooseMatchingCourier(anyFontItalic)); + assertEquals(PDType1Font.COURIER_BOLD_OBLIQUE, + DefaultFontTextDrawerDefaultFonts.chooseMatchingCourier(anyFontBoldItalic)); + + assertEquals(PDType1Font.HELVETICA, + DefaultFontTextDrawerDefaultFonts.chooseMatchingHelvetica(anyFont)); + assertEquals(PDType1Font.HELVETICA_BOLD, + DefaultFontTextDrawerDefaultFonts.chooseMatchingHelvetica(anyFontBold)); + assertEquals(PDType1Font.HELVETICA_OBLIQUE, + DefaultFontTextDrawerDefaultFonts.chooseMatchingHelvetica(anyFontItalic)); + assertEquals(PDType1Font.HELVETICA_BOLD_OBLIQUE, + DefaultFontTextDrawerDefaultFonts.chooseMatchingHelvetica(anyFontBoldItalic)); + + assertEquals(PDType1Font.TIMES_ROMAN, DefaultFontTextDrawerDefaultFonts.chooseMatchingTimes(anyFont)); + assertEquals(PDType1Font.TIMES_BOLD, + DefaultFontTextDrawerDefaultFonts.chooseMatchingTimes(anyFontBold)); + assertEquals(PDType1Font.TIMES_ITALIC, + DefaultFontTextDrawerDefaultFonts.chooseMatchingTimes(anyFontItalic)); + assertEquals(PDType1Font.TIMES_BOLD_ITALIC, + DefaultFontTextDrawerDefaultFonts.chooseMatchingTimes(anyFontBoldItalic)); + } + + @Test + public void testDefaultFontMapping() { + assertEquals(PDType1Font.HELVETICA, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode(Font.DIALOG))); + assertEquals(PDType1Font.HELVETICA, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode(Font.DIALOG_INPUT))); + assertEquals(PDType1Font.HELVETICA, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode("Arial"))); + + assertEquals(PDType1Font.COURIER, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode(Font.MONOSPACED))); + + assertEquals(PDType1Font.TIMES_ROMAN, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode(Font.SERIF))); + + assertEquals(PDType1Font.ZAPF_DINGBATS, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode("Dingbats"))); + + assertEquals(PDType1Font.SYMBOL, + DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode("Symbol"))); + + assertNull(DefaultFontTextDrawerDefaultFonts.mapDefaultFonts(Font.decode("Georgia"))); + } + +} \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2DTestBase.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2DTestBase.java new file mode 100644 index 0000000..20999cb --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2DTestBase.java @@ -0,0 +1,106 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import javax.imageio.ImageIO; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDFont; +import org.apache.pdfbox.pdmodel.font.PDFontFactory; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.util.Matrix; + +import java.awt.FontFormatException; +import java.awt.Graphics2D; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +class PdfBoxGraphics2DTestBase { + + enum Mode { + DefaultVectorized, FontTextIfPossible, ForceFontText, DefaultFontText + } + + void exportGraphic(String dir, String name, GraphicsExporter exporter) { + try { + PDDocument document = new PDDocument(); + PDFont pdArial = PDType1Font.HELVETICA; + File parentDir = new File("build/test/" + dir); + parentDir.mkdirs(); + + BufferedImage image = new BufferedImage(400, 400, BufferedImage.TYPE_4BYTE_ABGR); + Graphics2D imageGraphics = image.createGraphics(); + exporter.draw(imageGraphics); + imageGraphics.dispose(); + ImageIO.write(image, "PNG", new File(parentDir, name + ".png")); + + for (Mode m : Mode.values()) { + PDPage page = new PDPage(PDRectangle.A4); + document.addPage(page); + + PDPageContentStream contentStream = new PDPageContentStream(document, page); + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(document, 400, 400); + DefaultFontTextDrawer fontTextDrawer = null; + contentStream.beginText(); + contentStream.setStrokingColor(0f, 0f, 0f); + contentStream.setNonStrokingColor(0f, 0f, 0f); + contentStream.setFont(PDType1Font.HELVETICA_BOLD, 15); + contentStream.setTextMatrix(Matrix.getTranslateInstance(10, 800)); + contentStream.showText("Mode " + m); + contentStream.endText(); + switch (m) { + case FontTextIfPossible: + fontTextDrawer = new DefaultFontTextDrawer(); + registerFots(fontTextDrawer); + break; + case DefaultFontText: { + fontTextDrawer = new DefaultFontTextDrawerDefaultFonts(); + registerFots(fontTextDrawer); + break; + } + case ForceFontText: + fontTextDrawer = new DefaultFontTextForcedDrawer(); + registerFots(fontTextDrawer); + fontTextDrawer.registerFont("Arial", pdArial); + break; + case DefaultVectorized: + default: + break; + } + if (fontTextDrawer != null) { + pdfBoxGraphics2D.setFontTextDrawer(fontTextDrawer); + } + exporter.draw(pdfBoxGraphics2D); + pdfBoxGraphics2D.dispose(); + PDFormXObject appearanceStream = pdfBoxGraphics2D.getXFormObject(); + Matrix matrix = new Matrix(); + matrix.translate(0, 20); + contentStream.transform(matrix); + contentStream.drawForm(appearanceStream); + matrix.scale(1.5f, 1.5f); + matrix.translate(0, 100); + contentStream.transform(matrix); + contentStream.drawForm(appearanceStream); + contentStream.close(); + } + document.save(new File(parentDir, name + ".pdf")); + document.close(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private void registerFots(DefaultFontTextDrawer fontTextDrawer) { + fontTextDrawer.registerFont(new File( + "src/test/resources/org/xbib/graphics/graphics2d/pdfbox/DejaVuSerifCondensed.ttf")); + fontTextDrawer.registerFont(new File( + "src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Regular.ttf")); + } + + interface GraphicsExporter { + void draw(Graphics2D gfx) throws IOException, FontFormatException; + } + +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2dTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2dTest.java new file mode 100644 index 0000000..2a4c6c4 --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfBoxGraphics2dTest.java @@ -0,0 +1,282 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import javax.imageio.ImageIO; +import javax.imageio.ImageReader; +import org.junit.jupiter.api.Test; + +import java.awt.AlphaComposite; +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Composite; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.LinearGradientPaint; +import java.awt.RadialGradientPaint; +import java.awt.Rectangle; +import java.awt.RenderingHints; +import java.awt.TexturePaint; +import java.awt.font.TextAttribute; +import java.awt.geom.AffineTransform; +import java.awt.geom.Ellipse2D; +import java.awt.geom.Path2D; +import java.awt.geom.Rectangle2D; +import java.awt.geom.RoundRectangle2D; +import java.awt.image.BufferedImage; +import java.text.AttributedString; +import java.util.Iterator; + +public class PdfBoxGraphics2dTest extends PdfBoxGraphics2DTestBase { + + @Test + public void testNegativeShapesAndComposite() { + exportGraphic("simple", "negativeWithComposite", gfx -> { + RoundRectangle2D.Float rect = new RoundRectangle2D.Float(10f, 10f, 20f, 20f, 5f, + 6f); + + AffineTransform transformIdentity = new AffineTransform(); + AffineTransform transformMirrored = AffineTransform.getTranslateInstance(0, 100); + transformMirrored.scale(1, -0.5); + for (AffineTransform tf : new AffineTransform[]{transformIdentity, + transformMirrored}) { + gfx.setTransform(tf); + gfx.setColor(Color.red); + gfx.fill(rect); + gfx.setStroke(new BasicStroke(2f)); + gfx.draw(rect); + GradientPaint gp = new GradientPaint(10.0f, 25.0f, Color.blue, (float) 100, + (float) 100, Color.red); + gfx.setPaint(gp); + gfx.fill(AffineTransform.getTranslateInstance(30f, 20f) + .createTransformedShape(rect)); + Composite composite = gfx.getComposite(); + gfx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.8f)); + gfx.setColor(Color.cyan); + gfx.fillRect(15, 0, 40, 40); + gfx.setColor(Color.green); + gfx.drawRect(20, 10, 50, 50); + gfx.setColor(Color.magenta); + gfx.fill(new Ellipse2D.Double(20, 20, 100, 100)); + gfx.setColor(Color.orange); + gfx.fill(new Ellipse2D.Double(20, 20, -100, 100)); + gfx.setPaint(gp); + gfx.fill(new Ellipse2D.Double(10, 80, 20, 20)); + gfx.fill(new Ellipse2D.Double(10, 100, -20, -20)); + gfx.setComposite(composite); + } + + }); + } + + @Test + public void testGradients() { + exportGraphic("simple", "gradients", gfx -> { + LinearGradientPaint linearGradientPaint = new LinearGradientPaint(0, 0, 100, 200, + new float[]{0.0f, .2f, .4f, .9f, 1f}, + new Color[]{Color.YELLOW, Color.GREEN, Color.RED, Color.BLUE, + Color.GRAY}); + gfx.setPaint(linearGradientPaint); + gfx.fill(new Rectangle.Float(10, 10, 100, 50)); + gfx.fill(new Rectangle.Float(120, 10, 50, 50)); + gfx.fill(new Rectangle.Float(200, 10, 50, 100)); + RadialGradientPaint radialGradientPaint = new RadialGradientPaint(200, 200, 200, + new float[]{0.0f, .2f, .4f, .9f, 1f}, + new Color[]{Color.YELLOW, Color.GREEN, Color.RED, Color.BLUE, + Color.GRAY}); + gfx.setPaint(radialGradientPaint); + gfx.fill(new Rectangle.Float(10, 120, 100, 50)); + gfx.fill(new Rectangle.Float(120, 120, 50, 50)); + gfx.fill(new Rectangle.Float(200, 120, 50, 100)); + }); + } + + @Test + public void testBuildPatternFill() { + exportGraphic("simple", "patternfill", gfx -> { + Composite composite = gfx.getComposite(); + RadialGradientPaint radialGradientPaint = new RadialGradientPaint(200, 200, 200, + new float[]{0.0f, .2f, .4f, .9f, 1f}, + new Color[]{Color.YELLOW, Color.GREEN, Color.RED, Color.BLUE, + Color.GRAY}); + gfx.setPaint(radialGradientPaint); + gfx.setStroke(new BasicStroke(20)); + gfx.draw(new Ellipse2D.Double(100, 100, 80, 80)); + gfx.draw(new Ellipse2D.Double(150, 150, 50, 80)); + gfx.shear(0.4, 0.2); + gfx.draw(new Ellipse2D.Double(150, 150, 50, 80)); + gfx.setComposite(composite); + }); + } + + @Test + public void testDifferentFonts() { + exportGraphic("simple", "fonts", gfx -> { + Font sansSerif = new Font(Font.SANS_SERIF, Font.PLAIN, 15); + Font embeddedFont = Font.createFont(Font.TRUETYPE_FONT, + PdfBoxGraphics2dTest.class.getResourceAsStream("DejaVuSerifCondensed.ttf")) + .deriveFont(15f); + Font monoFont = Font.decode(Font.MONOSPACED).deriveFont(15f); + Font serifFont = Font.decode(Font.SERIF).deriveFont(15f); + int y = 50; + for (Font f : new Font[]{sansSerif, embeddedFont, monoFont, serifFont}) { + int x = 10; + gfx.setPaint(Color.BLACK); + gfx.setFont(f); + String txt = f.getFontName() + ": "; + gfx.drawString(txt, x, y); + x += gfx.getFontMetrics().stringWidth(txt); + + txt = "Normal "; + gfx.drawString(txt, x, y); + x += gfx.getFontMetrics().stringWidth(txt); + + gfx.setPaint(new CMYKColor(1f, 0.5f, 1f, 0.1f, 128)); + txt = "Bold "; + gfx.setFont(f.deriveFont(Font.BOLD)); + gfx.drawString(txt, x, y); + x += gfx.getFontMetrics().stringWidth(txt); + + gfx.setPaint(new CMYKColor(128, 128, 128, 0)); + txt = "Italic "; + gfx.setFont(f.deriveFont(Font.ITALIC)); + gfx.drawString(txt, x, y); + x += gfx.getFontMetrics().stringWidth(txt); + + gfx.setPaint(new CMYKColor(255, 255, 255, 255)); + txt = "Bold-Italic "; + gfx.setFont(f.deriveFont(Font.ITALIC | Font.BOLD)); + gfx.drawString(txt, x, y); + gfx.getFontMetrics().stringWidth(txt); + + y += 30; + } + + }); + } + + @Test + public void testImageEncoding() { + exportGraphic("imageenc", "imageenc", gfx -> { + BufferedImage img2 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("pixeltest.png")); + BufferedImage img3 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("Rose-ProPhoto.jpg")); + BufferedImage img4 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("Italy-P3.jpg")); + BufferedImage img5 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("16bit-image1.png")); + BufferedImage img6 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("16bit-image2.png")); + + gfx.drawImage(img2, 70, 50, 100, 50, null); + gfx.drawImage(img3, 30, 200, 75, 50, null); + gfx.drawImage(img4, 170, 10, 60, 40, null); + gfx.drawImage(img5, 270, 10, 16, 16, null); + gfx.drawImage(img5, 270, 30, 64, 64, null); + gfx.drawImage(img6, 270, 200, 100, 100, null); + }); + } + + @Test + public void testEvenOddRules() { + + exportGraphic("simple", "evenOdd", gfx -> { + gfx.setColor(Color.YELLOW); + gfx.fillPolygon(new int[]{80, 129, 0, 160, 31}, + new int[]{0, 152, 58, 58, 152}, 5); + Path2D.Double s = new Path2D.Double(); + s.moveTo(80, 0); + s.lineTo(129, 152); + s.lineTo(0, 58); + s.lineTo(160, 58); + s.lineTo(31, 152); + s.setWindingRule(Path2D.WIND_EVEN_ODD); + gfx.setColor(Color.BLUE); + gfx.translate(200, 0); + gfx.fill(s); + s.setWindingRule(Path2D.WIND_NON_ZERO); + gfx.setColor(Color.GREEN); + gfx.translate(0, 200); + gfx.fill(s); + }); + } + + @Test + public void testSimpleGraphics2d() { + Iterator readers = ImageIO.getImageReadersByFormatName("JPEG"); + while (readers.hasNext()) { + readers.next(); + } + exportGraphic("simple", "simple", gfx -> { + BufferedImage imgColorTest = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("colortest.png")); + BufferedImage img2 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("pixeltest.png")); + BufferedImage img3 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("Rose-ProPhoto.jpg")); + BufferedImage img4 = ImageIO + .read(PdfBoxGraphics2dTest.class.getResourceAsStream("Italy-P3.jpg")); + + gfx.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BICUBIC); + gfx.drawImage(imgColorTest, 70, 50, 100, 50, null); + + gfx.drawImage(img3, 30, 200, 75, 50, null); + gfx.drawImage(img3, 110, 200, 50, 50, null); + gfx.drawImage(img4, 170, 10, 60, 40, null); + + gfx.setColor(Color.YELLOW); + gfx.drawRect(20, 20, 100, 100); + gfx.setColor(Color.GREEN); + gfx.fillRect(10, 10, 50, 50); + + gfx.setColor(new CMYKColor(255, 128, 0, 128, 200)); + gfx.drawString("Hello World!", 30, 120); + gfx.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR); + gfx.drawImage(img2, 30, 50, 50, 50, null); + + Font font = new Font("SansSerif", Font.PLAIN, 30); + Font font2 = Font.createFont(Font.TRUETYPE_FONT, + PdfBoxGraphics2dTest.class.getResourceAsStream("DejaVuSerifCondensed.ttf")) + .deriveFont(20f); + final String words = "Valour fate kinship darkness"; + + AttributedString as1 = new AttributedString(words); + as1.addAttribute(TextAttribute.FONT, font); + + Rectangle2D valour = font2.getStringBounds("Valour", gfx.getFontRenderContext()); + GradientPaint gp = new GradientPaint(10.0f, 25.0f, Color.blue, + (float) valour.getWidth(), (float) valour.getHeight(), Color.red); + + gfx.setColor(Color.GREEN); + as1.addAttribute(TextAttribute.FOREGROUND, gp, 0, 6); + as1.addAttribute(TextAttribute.KERNING, TextAttribute.KERNING_ON, 0, 6); + as1.addAttribute(TextAttribute.FONT, font2, 0, 6); + as1.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, 7, 11); + as1.addAttribute(TextAttribute.BACKGROUND, Color.LIGHT_GRAY, 12, 19); + as1.addAttribute(TextAttribute.FONT, font2, 20, 28); + as1.addAttribute(TextAttribute.LIGATURES, TextAttribute.LIGATURES_ON, 20, 28); + as1.addAttribute(TextAttribute.STRIKETHROUGH, TextAttribute.STRIKETHROUGH_ON, 20, + 28); + + gfx.drawString(as1.getIterator(), 15, 160); + + // Hello World - in arabic and hebrew + Font font3 = new Font("SansSerif", Font.PLAIN, 40); + gfx.setFont(font3); + gfx.setColor(Color.BLACK); + gfx.drawString("مرحبا بالعالم", 200, 100); + gfx.setPaint( + new TexturePaint(imgColorTest, new Rectangle2D.Float(5f, 7f, 100f, 20f))); + gfx.drawString("مرحبا بالعالم", 200, 250); + gfx.drawString("שלום עולם", 200, 200); + + gfx.setClip(new Ellipse2D.Float(360, 360, 60, 80)); + gfx.fillRect(300, 300, 100, 100); + gfx.setClip(null); + gfx.fillRect(360, 360, 10, 10); + + }); + } + +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfRerenderTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfRerenderTest.java new file mode 100644 index 0000000..1080f17 --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/PdfRerenderTest.java @@ -0,0 +1,213 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.graphics.color.PDColor; +import org.apache.pdfbox.pdmodel.graphics.color.PDColorSpace; +import org.apache.pdfbox.pdmodel.graphics.color.PDDeviceRGB; +import org.apache.pdfbox.pdmodel.graphics.color.PDPattern; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.pdmodel.graphics.image.JPEGFactory; +import org.apache.pdfbox.pdmodel.graphics.image.LosslessFactory; +import org.apache.pdfbox.pdmodel.graphics.image.PDImage; +import org.apache.pdfbox.pdmodel.graphics.image.PDImageXObject; +import org.apache.pdfbox.rendering.PDFRenderer; +import org.apache.pdfbox.rendering.PageDrawer; +import org.apache.pdfbox.rendering.PageDrawerParameters; +import org.junit.jupiter.api.Test; + +import java.awt.BasicStroke; +import java.awt.Color; +import java.awt.Paint; +import java.awt.Shape; +import java.awt.Stroke; +import java.awt.image.BufferedImage; +import java.io.File; +import java.io.IOException; + +public class PdfRerenderTest { + + @Test + public void testPDFRerender() throws IOException { + rerenderPDF("heart.pdf"); + rerenderPDF("barChart.pdf"); + rerenderPDF("compuserver_msn_Ford_Focus.pdf"); + rerenderPDF("patternfill.pdf"); + } + + @Test + public void testSimpleRerender() throws IOException { + simplePDFRerender("antonio_sample.pdf"); + } + + @Test + public void testSimpleRerenderAsBitmap() throws IOException { + simplePDFRerenderAsBitmap("antonio_sample.pdf", false); + simplePDFRerenderAsBitmap("antonio_sample.pdf", true); + } + + public void simplePDFRerenderAsBitmap(String name, boolean lossless) throws IOException { + File parentDir = new File("build/test"); + parentDir.mkdirs(); + PDDocument document = new PDDocument(); + PDDocument sourceDoc = PDDocument.load(PdfRerenderTest.class.getResourceAsStream(name)); + + for (PDPage sourcePage : sourceDoc.getPages()) { + PDRectangle mediaBox = sourcePage.getMediaBox(); + PDPage rerenderedPage = new PDPage(mediaBox); + document.addPage(rerenderedPage); + try (PDPageContentStream cb = new PDPageContentStream(document, rerenderedPage)) { + + PDFRenderer pdfRenderer = new PDFRenderer(sourceDoc); + float targetDPI = 300; + BufferedImage bufferedImage = pdfRenderer + .renderImage(sourceDoc.getPages().indexOf(sourcePage), targetDPI / 72.0f); + + PDImageXObject image; + if (lossless) + image = LosslessFactory.createFromImage(document, bufferedImage); + else + image = JPEGFactory.createFromImage(document, bufferedImage, 0.7f); + + cb.drawImage(image, 0, 0, mediaBox.getWidth(), mediaBox.getHeight()); + } + } + document.save(new File(parentDir, "simple_bitmap_" + (lossless ? "" : "_jpeg_") + name)); + document.close(); + sourceDoc.close(); + } + + public void simplePDFRerender(String name) throws IOException { + File parentDir = new File("build/test"); + parentDir.mkdirs(); + + PDDocument document = new PDDocument(); + PDDocument sourceDoc = PDDocument.load(PdfRerenderTest.class.getResourceAsStream(name)); + + for (PDPage sourcePage : sourceDoc.getPages()) { + PDPage rerenderedPage = new PDPage(sourcePage.getMediaBox()); + document.addPage(rerenderedPage); + try (PDPageContentStream cb = new PDPageContentStream(document, rerenderedPage)) { + PdfBoxGraphics2D gfx = new PdfBoxGraphics2D(document, sourcePage.getMediaBox()); + PDFRenderer pdfRenderer = new PDFRenderer(sourceDoc); + pdfRenderer.renderPageToGraphics(sourceDoc.getPages().indexOf(sourcePage), gfx); + gfx.dispose(); + + PDFormXObject xFormObject = gfx.getXFormObject(); + cb.drawForm(xFormObject); + } + } + document.save(new File(parentDir, "simple_rerender" + name)); + document.close(); + sourceDoc.close(); + } + + private void rerenderPDF(String name) throws IOException { + File parentDir = new File("build/test"); + // noinspection ResultOfMethodCallIgnored + parentDir.mkdirs(); + + PDDocument document = new PDDocument(); + PDDocument sourceDoc = PDDocument.load(PdfRerenderTest.class.getResourceAsStream(name)); + + for (PDPage sourcePage : sourceDoc.getPages()) { + PDPage rerenderedPage = new PDPage(sourcePage.getMediaBox()); + document.addPage(rerenderedPage); + try (PDPageContentStream cb = new PDPageContentStream(document, rerenderedPage)) { + PdfBoxGraphics2D gfx = new PdfBoxGraphics2D(document, sourcePage.getMediaBox()); + + // Do overfill for red with a transparent green + gfx.setDrawControl(new DefaultDrawControl() { + boolean insideOwnDraw = false; + + @Override + public void afterShapeFill(Shape shape, IDrawControlEnv env) { + afterShapeDraw(shape, env); + } + + @Override + public void afterShapeDraw(Shape shape, IDrawControlEnv env) { + if (insideOwnDraw) + return; + insideOwnDraw = true; + Paint paint = env.getPaint(); + if (paint instanceof Color) { + if (paint.equals(Color.RED)) { + // We overfill with black a little bit + PdfBoxGraphics2D graphics = env.getGraphics(); + Stroke prevStroke = graphics.getStroke(); + float additinalStrokeWidth = 1f; + if (prevStroke instanceof BasicStroke) { + BasicStroke basicStroke = ((BasicStroke) prevStroke); + graphics.setStroke(new BasicStroke( + basicStroke.getLineWidth() + additinalStrokeWidth, + basicStroke.getEndCap(), basicStroke.getLineJoin(), + basicStroke.getMiterLimit(), basicStroke.getDashArray(), + basicStroke.getDashPhase())); + } else { + graphics.setStroke(new BasicStroke(additinalStrokeWidth)); + } + graphics.setPaint(new Color(0, 255, 0, 128)); + graphics.draw(shape); + + graphics.setPaint(paint); + graphics.setStroke(prevStroke); + } + } + insideOwnDraw = false; + } + }); + + PDFRenderer pdfRenderer = new PDFRenderer(sourceDoc) { + @Override + protected PageDrawer createPageDrawer(PageDrawerParameters parameters) + throws IOException { + return new PageDrawer(parameters) { + @Override + protected Paint getPaint(PDColor color) throws IOException { + PDColorSpace colorSpace = color.getColorSpace(); + + // We always must handle patterns recursive + if (colorSpace instanceof PDPattern) + return super.getPaint(color); + + // Now our special logic + if (colorSpace instanceof PDDeviceRGB) { + float[] components = color.getComponents(); + boolean allBlack = true; + for (float f : components) { + if (f > 0.0) { + allBlack = false; + break; + } + } + if (allBlack) { + return new CMYKColor(1f, 0.0f, 0.2f, 0.1f, 128); + } + } + // All other colors just stay the same... + return super.getPaint(color); + } + + @Override + public void drawImage(PDImage pdImage) { + // We dont like images, just skip them all + } + }; + } + }; + pdfRenderer.renderPageToGraphics(sourceDoc.getPages().indexOf(sourcePage), gfx); + gfx.dispose(); + + PDFormXObject xFormObject = gfx.getXFormObject(); + cb.drawForm(xFormObject); + } + } + + document.save(new File(parentDir, "rerendered_" + name)); + document.close(); + sourceDoc.close(); + } +} diff --git a/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/RenderSVGsTest.java b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/RenderSVGsTest.java new file mode 100644 index 0000000..ade6dc5 --- /dev/null +++ b/graphics2d-pdfbox/src/test/java/org/xbib/graphics/graphics2d/pdfbox/RenderSVGsTest.java @@ -0,0 +1,146 @@ +package org.xbib.graphics.graphics2d.pdfbox; + +import org.apache.batik.anim.dom.SAXSVGDocumentFactory; +import org.apache.batik.bridge.BridgeContext; +import org.apache.batik.bridge.DocumentLoader; +import org.apache.batik.bridge.GVTBuilder; +import org.apache.batik.bridge.UserAgent; +import org.apache.batik.bridge.UserAgentAdapter; +import org.apache.batik.gvt.GraphicsNode; +import org.apache.batik.util.XMLResourceDescriptor; +import org.apache.pdfbox.pdmodel.PDDocument; +import org.apache.pdfbox.pdmodel.PDPage; +import org.apache.pdfbox.pdmodel.PDPageContentStream; +import org.apache.pdfbox.pdmodel.common.PDRectangle; +import org.apache.pdfbox.pdmodel.font.PDType1Font; +import org.apache.pdfbox.pdmodel.graphics.form.PDFormXObject; +import org.apache.pdfbox.util.Matrix; +import org.junit.jupiter.api.Test; +import org.w3c.dom.Document; + +import java.awt.Graphics2D; +import java.awt.color.ICC_Profile; +import java.io.File; +import java.io.IOException; + +public class RenderSVGsTest extends PdfBoxGraphics2DTestBase { + + @Test + public void testSVGs() throws IOException { + renderSVG("barChart.svg", 0.45); + renderSVG("gump-bench.svg", 1); + renderSVG("json.svg", 150); + renderSVG("heart.svg", 200); + renderSVG("displayWebStats.svg", 200); + renderSVG("compuserver_msn_Ford_Focus.svg", 0.7); + renderSVG("watermark.svg", 0.4); + } + + @Test + public void renderFailureCases() throws IOException { + // renderSVG("openhtml_536.svg", 1); + renderSVG("openhtml_538_gradient.svg", .5); + } + + @Test + public void testGradientSVGEmulateObjectBoundingBox() throws IOException { + renderSVG("long-gradient.svg", 0.55); + renderSVG("tall-gradient.svg", 0.33); + renderSVG("near-square-gradient.svg", 0.30); + renderSVG("square-gradient.svg", 0.55); + renderSVG("tall-gradient-downward-slope.svg", 0.33); + renderSVG("horizontal-gradient.svg", 0.55); + } + + @Test + public void testSVGinCMYKColorspace() throws IOException { + renderSVGCMYK("atmospheric-composiition.svg", 0.7); + } + + private void renderSVG(String name, final double scale) throws IOException { + String uri = RenderSVGsTest.class.getResource(name).toString(); + + // create the document + String parser = XMLResourceDescriptor.getXMLParserClassName(); + SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); + Document document = f.createDocument(uri, RenderSVGsTest.class.getResourceAsStream(name)); + + // create the GVT + UserAgent userAgent = new UserAgentAdapter(); + DocumentLoader loader = new DocumentLoader(userAgent); + BridgeContext bctx = new BridgeContext(userAgent, loader); + bctx.setDynamicState(BridgeContext.STATIC); + GVTBuilder builder = new GVTBuilder(); + final GraphicsNode gvtRoot = builder.build(bctx, document); + + this.exportGraphic("svg", name.replace(".svg", ""), new GraphicsExporter() { + @Override + public void draw(Graphics2D gfx) { + gfx.scale(scale, scale); + gvtRoot.paint(gfx); + } + }); + } + + private void renderSVGCMYK(String name, final double scale) throws IOException { + String uri = RenderSVGsTest.class.getResource(name).toString(); + + // create the document + String parser = XMLResourceDescriptor.getXMLParserClassName(); + SAXSVGDocumentFactory f = new SAXSVGDocumentFactory(parser); + Document document = f.createDocument(uri, RenderSVGsTest.class.getResourceAsStream(name)); + + // create the GVT + UserAgent userAgent = new UserAgentAdapter(); + DocumentLoader loader = new DocumentLoader(userAgent); + BridgeContext bctx = new BridgeContext(userAgent, loader); + bctx.setDynamicState(BridgeContext.STATIC); + GVTBuilder builder = new GVTBuilder(); + final GraphicsNode gvtRoot = builder.build(bctx, document); + + PDDocument pdfDocument = new PDDocument(); + + File parentDir = new File("build/test/svg"); + parentDir.mkdirs(); + + PDPage page = new PDPage(PDRectangle.A4); + pdfDocument.addPage(page); + + PDPageContentStream contentStream = new PDPageContentStream(pdfDocument, page); + + PdfBoxGraphics2D pdfBoxGraphics2D = new PdfBoxGraphics2D(pdfDocument, 400, 400); + + ICC_Profile icc_profile = ICC_Profile.getInstance(PDDocument.class.getResourceAsStream( + "/org/apache/pdfbox/resources/icc/ISOcoated_v2_300_bas.icc")); + DefaultColorMapper colorMapper = new RGBtoCMYKColorMapper(icc_profile, pdfDocument); + pdfBoxGraphics2D.setColorMapper(colorMapper); + + FontTextDrawer fontTextDrawer = null; + contentStream.beginText(); + contentStream.setStrokingColor(0.0f, 0.0f, 0.0f, 1.0f); + contentStream.setNonStrokingColor(0.0f, 0.0f, 0.0f, 1.0f); + contentStream.setFont(PDType1Font.HELVETICA_BOLD, 15); + contentStream.setTextMatrix(Matrix.getTranslateInstance(10, 800)); + contentStream.showText("Mode: CMYK colorspace"); + contentStream.endText(); + fontTextDrawer = new DefaultFontTextDrawer(); + + pdfBoxGraphics2D.setFontTextDrawer(fontTextDrawer); + + pdfBoxGraphics2D.scale(scale, scale); + gvtRoot.paint(pdfBoxGraphics2D); + pdfBoxGraphics2D.dispose(); + + PDFormXObject appearanceStream = pdfBoxGraphics2D.getXFormObject(); + Matrix matrix = new Matrix(); + matrix.translate(0, 300); + contentStream.transform(matrix); + contentStream.drawForm(appearanceStream); + + contentStream.close(); + + String baseName = name.substring(0, name.lastIndexOf('.')); + pdfDocument.save(new File(parentDir, baseName + ".pdf")); + pdfDocument.close(); + } +} diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/16bit-image1.png b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/16bit-image1.png new file mode 100644 index 0000000..2b07778 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/16bit-image1.png differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/16bit-image2.png b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/16bit-image2.png new file mode 100644 index 0000000..1cebe40 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/16bit-image2.png differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/DejaVuSerifCondensed.ttf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/DejaVuSerifCondensed.ttf new file mode 100644 index 0000000..d3959b3 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/DejaVuSerifCondensed.ttf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/Italy-P3.jpg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/Italy-P3.jpg new file mode 100644 index 0000000..0cb1e10 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/Italy-P3.jpg differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/Rose-ProPhoto.jpg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/Rose-ProPhoto.jpg new file mode 100644 index 0000000..becc439 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/Rose-ProPhoto.jpg differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Bold.ttf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Bold.ttf new file mode 100755 index 0000000..20a35d1 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Bold.ttf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Light.ttf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Light.ttf new file mode 100755 index 0000000..1b3f910 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Light.ttf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Regular.ttf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Regular.ttf new file mode 100755 index 0000000..9f8759a Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/Antonio-Regular.ttf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/SIL Open Font License.txt b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/SIL Open Font License.txt new file mode 100755 index 0000000..83b869d --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio/SIL Open Font License.txt @@ -0,0 +1,43 @@ +Copyright (c) 2011-12, vernon adams (vern@newtypography.co.uk), with Reserved Font Names 'Antonio' + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide development of collaborative font projects, to support the font creation efforts of academic and linguistic communities, and to provide a free and open framework in which fonts may be shared and improved in partnership with others. + +The OFL allows the licensed fonts to be used, studied, modified and redistributed freely as long as they are not sold by themselves. The fonts, including any derivative works, can be bundled, embedded, redistributed and/or sold with any software provided that any reserved names are not used by derivative works. The fonts and derivatives, however, cannot be released under any other type of license. The requirement for fonts to remain under this license does not apply to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright Holder(s) under this license and clearly marked as such. This may include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the copyright statement(s). + +"Original Version" refers to the collection of Font Software components as distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, or substituting -- in part or in whole -- any of the components of the Original Version, by changing formats or by porting the Font Software to a new environment. + +"Author" refers to any designer, engineer, programmer, technical writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining a copy of the Font Software, to use, study, copy, merge, embed, modify, redistribute, and sell modified and unmodified copies of the Font Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, redistributed and/or sold with any software, provided that each copy contains the above copyright notice and this license. These can be included either as stand-alone text files, human-readable headers or in the appropriate machine-readable metadata fields within text or binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font Name(s) unless explicit written permission is granted by the corresponding Copyright Holder. This restriction only applies to the primary font name as presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font Software shall not be used to promote, endorse or advertise any Modified Version, except to acknowledge the contribution(s) of the Copyright Holder(s) and the Author(s) or with their explicit written permission. + +5) The Font Software, modified or unmodified, in part or in whole, must be distributed entirely under this license, and must not be distributed under any other license. The requirement for fonts to remain under this license does not apply to any document created using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE FONT SOFTWARE. \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio_sample.pdf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio_sample.pdf new file mode 100644 index 0000000..afe17ce Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/antonio_sample.pdf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/atmospheric-composiition.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/atmospheric-composiition.svg new file mode 100644 index 0000000..7b0b35e --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/atmospheric-composiition.svg @@ -0,0 +1 @@ +Created with Highcharts x.x.xComposition of the AtmosphereNitrogen: 78.1%Oxygen: 21.0%Argon: 0.9%NitrogenOxygenArgon \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/barChart.pdf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/barChart.pdf new file mode 100644 index 0000000..4e392f2 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/barChart.pdf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/barChart.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/barChart.svg new file mode 100644 index 0000000..f04b690 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/barChart.svg @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + Bar Chart + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Shoe + Car + Travel + Computer + + 0 + 10 + 20 + 30 + 40 + 50 + 60 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/colortest.png b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/colortest.png new file mode 100644 index 0000000..811689c Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/colortest.png differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/compuserver_msn_Ford_Focus.pdf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/compuserver_msn_Ford_Focus.pdf new file mode 100644 index 0000000..42215af Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/compuserver_msn_Ford_Focus.pdf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/compuserver_msn_Ford_Focus.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/compuserver_msn_Ford_Focus.svg new file mode 100644 index 0000000..7152f30 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/compuserver_msn_Ford_Focus.svg @@ -0,0 +1,3051 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/displayWebStats.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/displayWebStats.svg new file mode 100644 index 0000000..e687b05 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/displayWebStats.svg @@ -0,0 +1,147 @@ + + + + + Interactive Web Statistics via SVG + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + HELP + + + + MODE + + + + Unique Weekly Visits By Browser + + + + + Chrome/OtherChrome/2.0.xChrome/1.0.xChrome/0.5-IE/OtherIE/8.0xIE/7.0xIE/6.0xIE/5.5-Firefox/OtherFirefox/3.1.xFirefox/3.0.xFirefox/2.0.xFirefox/1.5-Safari/OtherSafari/4.0.xSafari/3.1.xSafari/3.0.xSafari/2.0-Opera/OtherOpera/10.0Opera/9.5xOpera/9.0xOpera/8.5- + + + + 150030004500600075002006200720082009 + 2006200720082009 + + + + + + + Opera/8.5-Opera/9.0xOpera/9.5xOpera/10.0Opera/OtherSafari/2.0-Safari/3.0.xSafari/3.1.xSafari/4.0.xSafari/OtherFirefox/1.5-Firefox/2.0.xFirefox/3.0.xFirefox/3.1.xFirefox/OtherIE/5.5-IE/6.0xIE/7.0xIE/8.0xIE/OtherChrome/0.5-Chrome/1.0.xChrome/2.0.xChrome/Other%8.5-Opera4%%9.0x%9.5x%10.0%X%2.0-Safari3%%3.0.x%3.1.x%4.0.x%X%1.5-Firefox27%3%2.0.x21%3.0.x%3.1.x%X37%5.5-IE65%18%6.0x10%7.0x%8.0x%X%0.5-Chrome2%%1.0.x%2.0.x%XDistribution for 2009-01-09Total Browser Hits: 1937 + + + + + + + + + Browser Legend + Opera 8.5-Opera 9.0xOpera 9.5xOpera 10.0Opera OtherSafari 2.0-Safari 3.0.xSafari 3.1.xSafari 4.0.xSafari OtherFirefox 1.5-Firefox 2.0.xFirefox 3.0.xFirefox 3.1.xFirefox OtherIE 5.5-IE 6.0xIE 7.0xIE 8.0xIE OtherChrome 0.5-Chrome 1.0.xChrome 2.0.xChrome Other + + + + + Click and drag these red and green grippies! + + + + + + + + + + + + + + This is a scrollbar + + + + + + + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/gump-bench.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/gump-bench.svg new file mode 100644 index 0000000..ff7aabc --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/gump-bench.svg @@ -0,0 +1,72 @@ + + + + Bench, Forest Gump + + + + + + + + + + + + + + + + + + + + + + + + + + + + Box of Chocolates + + + + + + + + + + + + + + + + + Bench + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/heart.pdf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/heart.pdf new file mode 100644 index 0000000..0ce64ea Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/heart.pdf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/heart.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/heart.svg new file mode 100644 index 0000000..1fad09f --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/heart.svg @@ -0,0 +1,4 @@ + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/horizontal-gradient.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/horizontal-gradient.svg new file mode 100644 index 0000000..176fbe2 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/horizontal-gradient.svg @@ -0,0 +1 @@ +Created with Highcharts x.x.xNo data to display \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/json.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/json.svg new file mode 100644 index 0000000..7ec7a92 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/json.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/long-gradient.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/long-gradient.svg new file mode 100644 index 0000000..fb0b0c7 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/long-gradient.svg @@ -0,0 +1,58 @@ + + Created with Highcharts x.x.x + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + No data to display + + + + + + + + \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/near-square-gradient.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/near-square-gradient.svg new file mode 100644 index 0000000..d109da4 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/near-square-gradient.svg @@ -0,0 +1 @@ +Created with Highcharts x.x.xNo data to display \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/openhtml_536.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/openhtml_536.svg new file mode 100644 index 0000000..24a3a77 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/openhtml_536.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/openhtml_538_gradient.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/openhtml_538_gradient.svg new file mode 100644 index 0000000..908ce0b --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/openhtml_538_gradient.svg @@ -0,0 +1,997 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/patternfill.pdf b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/patternfill.pdf new file mode 100644 index 0000000..2092f09 Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/patternfill.pdf differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/pixeltest.png b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/pixeltest.png new file mode 100644 index 0000000..daef63d Binary files /dev/null and b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/pixeltest.png differ diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/square-gradient.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/square-gradient.svg new file mode 100644 index 0000000..96ebfc6 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/square-gradient.svg @@ -0,0 +1 @@ +Created with Highcharts x.x.xNo data to display \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/tall-gradient-downward-slope.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/tall-gradient-downward-slope.svg new file mode 100644 index 0000000..37f874b --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/tall-gradient-downward-slope.svg @@ -0,0 +1 @@ +Created with Highcharts x.x.xNo data to display \ No newline at end of file diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/tall-gradient.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/tall-gradient.svg new file mode 100644 index 0000000..e793d18 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/tall-gradient.svg @@ -0,0 +1,2 @@ +Created with Highcharts x.x.xNo data to display + diff --git a/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/watermark.svg b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/watermark.svg new file mode 100644 index 0000000..7aa6bd2 --- /dev/null +++ b/graphics2d-pdfbox/src/test/resources/org/xbib/graphics/graphics2d/pdfbox/watermark.svg @@ -0,0 +1,19 @@ + + + + + + TEST WATERMARK + + + test watermark + + + + + + + + + + \ No newline at end of file diff --git a/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/IndentCharacters.java b/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/IndentCharacters.java index c9f1a5a..99e474f 100644 --- a/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/IndentCharacters.java +++ b/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/IndentCharacters.java @@ -318,24 +318,4 @@ public class IndentCharacters { } } - - public static void main(String[] args) { - Pattern PATTERN = Pattern// - .compile("^-(!)|^([ ]*)-(-)(\\{(\\d*)(em|pt)?\\})?|^([ ]*)-(\\+)(\\{(.)?:(\\d*)(em|pt)?\\})?|^([ ]*)-(#)(\\{((?!:).)?(.+)?:((\\d*))((em|pt))?\\})?"); - Matcher matcher = PATTERN.matcher(" -#{d:3em}"); - System.out.println("matches: " + matcher.find()); - if (!matcher.matches()) { - System.err.println("exit"); - return; - } - System.out.println("start: " + matcher.start()); - System.out.println("end: " + matcher.end()); - System.out.println("groups: " + matcher.groupCount()); - for (int i = 0; i < matcher.groupCount(); i++) { - System.out.println("group " + i + ": '" + matcher.group(i) + "'"); - } - // 2 - -> 1: blanks, 4: size, 5: unit - // 7 + -> 6: blanks, 9: sign, 10: size, 11: unit - // 11 # -> 12: blanks, 15: number-sign, 16: size, 18: unit - } } diff --git a/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/annotations/AnnotationCharacters.java b/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/annotations/AnnotationCharacters.java index a7e8fc4..8780105 100644 --- a/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/annotations/AnnotationCharacters.java +++ b/layout-pdfbox/src/main/java/org/xbib/graphics/layout/pdfbox/text/annotations/AnnotationCharacters.java @@ -16,7 +16,7 @@ import java.util.regex.Pattern; */ public class AnnotationCharacters { - private final static List> FACTORIES = new CopyOnWriteArrayList>(); + private final static List> FACTORIES = new CopyOnWriteArrayList<>(); static { register(new HyperlinkControlCharacterFactory()); @@ -218,10 +218,6 @@ public class AnnotationCharacters { private static Float defaultBaselineOffsetScale; private final UnderlineAnnotation line; - protected UnderlineControlCharacter() { - this(null, null); - } - protected UnderlineControlCharacter(String baselineOffsetScaleValue, String lineWeightValue) { super("UNDERLINE", UnderlineControlCharacterFactory.TO_ESCAPE); @@ -285,8 +281,8 @@ public class AnnotationCharacters { extends ControlCharacter { protected AnnotationControlCharacter(final String description, - final String charaterToEscape) { - super(description, charaterToEscape); + final String characterToEscape) { + super(description, characterToEscape); } /** @@ -300,25 +296,4 @@ public class AnnotationCharacters { public abstract Class getAnnotationType(); } - - public static void main(String[] args) { - Pattern PATTERN = Pattern - .compile("(? 1: blanks, 4: size, 5: unit - // 7 + -> 6: blanks, 9: sign, 10: size, 11: unit - // 11 # -> 12: blanks, 15: number-sign, 16: size, 18: unit - } - } diff --git a/settings.gradle b/settings.gradle index 29bb9ff..262e08d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,4 +2,5 @@ include 'png' include 'io-vector' include 'chart' include 'barcode' -include 'layout-pdfbox' \ No newline at end of file +include 'layout-pdfbox' +include 'graphics2d-pdfbox' \ No newline at end of file