/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.quercus.lib; import com.caucho.quercus.QuercusException; import com.caucho.quercus.QuercusModuleException; import com.caucho.quercus.annotation.NotNull; import com.caucho.quercus.annotation.Optional; import com.caucho.quercus.annotation.ReturnNullAsFalse; import com.caucho.quercus.env.*; import com.caucho.quercus.module.AbstractQuercusModule; import com.caucho.util.L10N; import com.caucho.util.LruCache; import com.caucho.vfs.Path; import com.caucho.vfs.ReadStream; import com.caucho.vfs.WriteStream; import javax.imageio.ImageIO; import java.awt.*; import java.awt.color.ColorSpace; import java.awt.geom.*; import java.awt.image.*; import java.io.*; import java.util.*; import java.util.logging.*; /** * PHP image */ public class ImageModule extends AbstractQuercusModule { private static final Logger log = Logger.getLogger(ImageModule.class.getName()); private static final L10N L = new L10N(ImageModule.class); public static final long IMG_GIF = 0x1; public static final long IMG_JPG = 0x2; public static final long IMG_JPEG = 0x2; public static final long IMG_PNG = 0x4; public static final long IMG_WBMP = 0x8; public static final long IMG_XPM = 0x10; public static final int IMAGETYPE_GIF = 1; public static final int IMAGETYPE_JPG = 2; public static final int IMAGETYPE_JPEG = 2; public static final int IMAGETYPE_PNG = 3; public static final int IMAGETYPE_SWF = 4; public static final int IMAGETYPE_PSD = 5; public static final int IMAGETYPE_BMP = 6; public static final int IMAGETYPE_TIFF_II = 7; public static final int IMAGETYPE_TIFF_MM = 8; public static final int IMAGETYPE_JPC = 9; public static final int IMAGETYPE_JP2 = 10; public static final int IMAGETYPE_JPX = 11; public static final int IMAGETYPE_JB2 = 12; public static final int IMAGETYPE_SWC = 13; public static final int IMAGETYPE_IFF = 14; public static final int IMAGETYPE_WBMP = 15; public static final int IMAGETYPE_XBM = 16; public static final int IMG_COLOR_STYLED = -2; public static final int IMG_COLOR_BRUSHED = -3; private static final int PNG_IHDR = pngCode("IHDR"); public static final int IMG_ARC_PIE = 0; public static final int IMG_ARC_CHORD = 1; public static final int IMG_ARC_NOFILL = 2; public static final int IMG_ARC_EDGED = 4; public static final int IMG_FILTER_NEGATE = 0; public static final int IMG_FILTER_GRAYSCALE = 1; public static final int IMG_FILTER_BRIGHTNESS = 2; public static final int IMG_FILTER_CONTRAST = 3; public static final int IMG_FILTER_COLORIZE = 4; public static final int IMG_FILTER_EDGEDETECT = 5; public static final int IMG_FILTER_EMBOSS = 6; public static final int IMG_FILTER_GAUSSIAN_BLUR = 7; public static final int IMG_FILTER_SELECTIVE_BLUR = 8; public static final int IMG_FILTER_MEAN_REMOVAL = 9; public static final int IMG_FILTER_SMOOTH = 10; private static LruCache<StringValue,Font> _fontMap = new LruCache<StringValue,Font>(4096); public String []getLoadedExtensions() { return new String[] { "gd" }; } /** * Retrieve information about the currently installed GD library */ public static Value gd_info() { return (new ArrayValueImpl() .append(StringValue.create("GD Version"), // ] => 2.0 or higher StringValue.create("2.0 or higher")) .append(StringValue.create("FreeType Support"), // ] => 1 BooleanValue.TRUE) .append( StringValue.create("FreeType Linkage"), // ] => with freetype StringValue.create("with freetype")) .append(StringValue.create("T1Lib Support"), // ] => 1 BooleanValue.TRUE) .append(StringValue.create("GIF Read Support"), // ] => 1 BooleanValue.TRUE) .append(StringValue.create("GIF Create Support"), // ] => 1 BooleanValue.TRUE) .append(StringValue.create("JPG Support"), // ] => 1 BooleanValue.TRUE) .append(StringValue.create("PNG Support"), // ] => 1 BooleanValue.TRUE) .append(StringValue.create("WBMP Support"), // ] => 1 BooleanValue.TRUE) .append(StringValue.create("XPM Support"), // ] => BooleanValue.FALSE) .append(StringValue.create("XBM Support"), // ] => BooleanValue.FALSE) .append(StringValue.create("JIS-mapped Japanese Font Support"), // ] => BooleanValue.FALSE)); } /** * Returns the environment value. */ public Value getimagesize(Env env, Path file, @Optional ArrayValue imageArray) { if (! file.canRead()) return BooleanValue.FALSE; ImageInfo info = new ImageInfo(); ReadStream is = null; try { is = file.openRead(); if (! parseImageSize(is, info)) return BooleanValue.FALSE; } catch (Exception e) { log.log(Level.FINE, e.toString(), e); return BooleanValue.FALSE; } finally { is.close(); } if (imageArray == null) imageArray = new ArrayValueImpl(); imageArray.put(LongValue.create(info._width)); imageArray.put(LongValue.create(info._height)); imageArray.put(LongValue.create(info._type)); imageArray.put(env.createString("width=\"" + info._width + "\" height=\"" + info._height + "\"")); if (info._bits >= 0) imageArray.put(env.createString("bits"), LongValue.create(info._bits)); if (info._type == IMAGETYPE_JPEG) imageArray.put("channels", 3); if (info._mime != null) imageArray.put("mime", info._mime); return imageArray; } /** * Get file extension for image type */ public static Value image_type_to_extension(int imageType, boolean dot) { switch(imageType) { case IMAGETYPE_GIF: return StringValue.create(dot ? ".gif" : "gif"); case IMAGETYPE_JPG: return StringValue.create(dot ? ".jpg" : "jpg"); case IMAGETYPE_PNG: return StringValue.create(dot ? ".png" : "png"); case IMAGETYPE_SWF: return StringValue.create(dot ? ".swf" : "swf"); case IMAGETYPE_PSD: return StringValue.create(dot ? ".psd" : "psd"); case IMAGETYPE_BMP: return StringValue.create(dot ? ".bmp" : "bmp"); case IMAGETYPE_TIFF_II: return StringValue.create(dot ? ".tiff" : "tiff"); case IMAGETYPE_TIFF_MM: return StringValue.create(dot ? ".tiff" : "tiff"); case IMAGETYPE_JPC: return StringValue.create(dot ? ".jpc" : "jpc"); case IMAGETYPE_JP2: return StringValue.create(dot ? ".jp2" : "jp2"); case IMAGETYPE_JPX: return StringValue.create(dot ? ".jpf" : "jpf"); case IMAGETYPE_JB2: return StringValue.create(dot ? ".jb2" : "jb2"); case IMAGETYPE_SWC: return StringValue.create(dot ? ".swc" : "swc"); case IMAGETYPE_IFF: return StringValue.create(dot ? ".iff" : "iff"); case IMAGETYPE_WBMP: return StringValue.create(dot ? ".wbmp" : "wbmp"); case IMAGETYPE_XBM: return StringValue.create(dot ? ".xbm" : "xbm"); } throw new QuercusException("unknown imagetype " + imageType); } /** * Get Mime-Type for image-type returned by getimagesize, exif_read_data, * exif_thumbnail, exif_imagetype */ public static Value image_type_to_mime_type(int imageType) { switch(imageType) { case IMAGETYPE_GIF: return StringValue.create("image/gif"); case IMAGETYPE_JPG: return StringValue.create("image/jpeg"); case IMAGETYPE_PNG: return StringValue.create("image/png"); case IMAGETYPE_SWF: return StringValue.create("application/x-shockwave-flash"); case IMAGETYPE_PSD: return StringValue.create("image/psd"); case IMAGETYPE_BMP: return StringValue.create("image/bmp"); case IMAGETYPE_TIFF_II: return StringValue.create("image/tiff"); case IMAGETYPE_TIFF_MM: return StringValue.create("image/tiff"); case IMAGETYPE_JPC: return StringValue.create("application/octet-stream"); case IMAGETYPE_JP2: return StringValue.create("image/jp2"); case IMAGETYPE_JPX: return StringValue.create("application/octet-stream"); case IMAGETYPE_JB2: return StringValue.create("application/octet-stream"); case IMAGETYPE_SWC: return StringValue.create("application/x-shockwave-flash"); case IMAGETYPE_IFF: return StringValue.create("image/iff"); case IMAGETYPE_WBMP: return StringValue.create("image/vnd.wap.wbmp"); case IMAGETYPE_XBM: return StringValue.create("image/xbm"); } throw new QuercusException("unknown imageType " + imageType); } // XXX: image2wbmp /** * Returns a copy of the current transform */ public static AffineTransform image_get_transform(QuercusImage image) { if (image == null) return null; return image.getGraphics().getTransform(); } /** * Returns a copy of the current transform */ public static boolean image_set_transform(QuercusImage image, AffineTransform transform) { if (image == null) return false; image.getGraphics().setTransform(transform); return true; } /** * Set the blending mode for an image */ public static boolean imagealphablending(QuercusImage image, boolean useAlphaBlending) { image.getGraphics().setComposite(useAlphaBlending ? AlphaComposite.SrcOver : AlphaComposite.Src); return true; } /** * Should antialias functions be used or not */ public static boolean imageantialias(QuercusImage image, boolean useAntiAliasing) { image.getGraphics().setRenderingHint(RenderingHints.KEY_ANTIALIASING, useAntiAliasing ? RenderingHints.VALUE_ANTIALIAS_ON : RenderingHints.VALUE_ANTIALIAS_OFF); return true; } /** * Draw a partial ellipse */ public static boolean imagearc(QuercusImage image, double cx, double cy, double width, double height, double start, double end, int color) { Arc2D arc = new Arc2D.Double(cx - width / 2, cy - height / 2, width, height, -1 * start, -1 * (end - start), Arc2D.OPEN); image.stroke(arc, color); return true; } /** * Draw a character horizontally */ public static boolean imagechar(QuercusImage image, int font, int x, int y, String c, int color) { Graphics2D g = image.getGraphics(); g.setColor(intToColor(color)); Font awtfont = image.getFont(font); int height = image.getGraphics().getFontMetrics(awtfont).getAscent(); g.setFont(awtfont); g.drawString(c.substring(0, 1), x, y + height); return true; } /** * Draw a character vertically */ public static boolean imagecharup(QuercusImage image, int font, int x, int y, String c, int color) { Graphics2D g = (Graphics2D)image.getGraphics().create(); g.rotate(-1 * Math.PI / 2); g.setColor(intToColor(color)); Font awtfont = image.getFont(font); int height = image.getGraphics().getFontMetrics(awtfont).getAscent(); g.setFont(awtfont); g.drawString(c.substring(0, 1), -1 * y, x + height); return true; } /** * Allocate a color for an image */ public static long imagecolorallocate(QuercusImage image, int r, int g, int b) { if (image != null) return image.allocateColor(r, g, b); else return 0; } /** * Allocate a color for an image */ public static long imagecolorallocatealpha(QuercusImage image, int r, int g, int b, int a) { // PHP's alpha values are inverted and only 7 bits. int alpha = 0x7f - (a & 0xff); return ((alpha << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)); } /** * Get the index of the color of a pixel */ public static long imagecolorat(QuercusImage image, int x, int y) { return image.getPixel(x, y); } /** * Get the index of the closest color to the specified color */ public static long imagecolorclosest(QuercusImage image, int r, int g, int b) { return imagecolorallocate(image, r, g, b); } /** * Get the index of the closest color to the specified color + alpha */ public static long imagecolorclosestalpha(QuercusImage image, int r, int g, int b, int a) { return imagecolorallocatealpha(image, r, g, b, a); } /** * Get the index of the color which has the hue, white and blackness * nearest to the given color */ public static long imagecolorclosesthwb(QuercusImage image, int r, int g, int b) { throw new QuercusException("imagecolorclosesthwb is not supported"); } /** * De-allocate a color for an image */ public static boolean imagecolordeallocate(QuercusImage image, int rgb) { // no-op return true; } /** * Get the index of the specified color */ public static long imagecolorexact(QuercusImage image, int r, int g, int b) { return imagecolorallocate(image, r, g, b); } /** * Get the index of the specified color + alpha */ public static long imagecolorexactalpha(QuercusImage image, int r, int g, int b, int a) { return imagecolorallocatealpha(image, r, g, b, a); } /** * Makes the colors of the palette version of an image more closely * match the true color version */ public static boolean imagecolormatch(QuercusImage image1, QuercusImage image2) { // no-op return true; } /** * Get the index of the specified color or its closest possible alternative */ public static long imagecolorresolve(QuercusImage image, int r, int g, int b) { return imagecolorallocate(image, r, g, b); } /** * Get the index of the specified color + alpha or its closest possible * alternative */ public static long imagecolorresolvealpha(QuercusImage image, int r, int g, int b, int a) { return imagecolorallocatealpha(image, r, g, b, a); } /** * Set the color for the specified palette index */ public static boolean imagecolorset(QuercusImage image, int index, int r, int g, int b) { // no-op since we currently only support true-color, full-alpha channel return true; } /** * Get the colors for an index */ public static ArrayValue imagecolorsforindex(QuercusImage image, int argb) { ArrayValue arrayValue = new ArrayValueImpl(); arrayValue.put("red", (argb >> 16) & 0xff); arrayValue.put("green", (argb >> 8) & 0xff); arrayValue.put("blue", (argb >> 0) & 0xff); // PHP's alpha is backwards from the rest of the world... int alpha = 0x7f - ((argb >> 24) & 0xff); arrayValue.put("alpha", alpha); return arrayValue; } /** * Find out the number of colors in an image's palette */ public static Value imagecolorstotal() { return LongValue.create(0); } /** * Define a color as transparent */ public static long imagecolortransparent(QuercusImage image, @Optional int color) { // form that includes the optional argument is a no-op since we // currently only support true-color, full-alpha channel return 0xFF000000; } /** * Apply a 3x3 convolution matrix, using coefficient div and offset */ public static boolean imageconvolution(QuercusImage image, ArrayValue matrix, double div, double offset) { // XXX: implement div and offset float[] kernelValues = new float[9]; for (int y = 0; y < 3; y++) { for (int x = 0; x < 3; x++) { kernelValues[x + y * 3] = (float) matrix.get(LongValue.create(y)) .get(LongValue.create(x)).toDouble(); } } ConvolveOp convolveOp = new ConvolveOp(new Kernel(3, 3, kernelValues), ConvolveOp.EDGE_NO_OP, null); BufferedImage bufferedImage = convolveOp.filter(image._bufferedImage, null); image._bufferedImage.getGraphics().drawImage(bufferedImage, 1, 0, null); return true; } /** * Copy part of an image */ public static boolean imagecopy(QuercusImage dest, QuercusImage src, int dx, int dy, int sx, int sy, int w, int h) { dest.getGraphics().drawImage(src._bufferedImage, dx, dy, dx + w, dy + h, sx, sy, sx + w, sy + h, null); return true; } /** * Copy and merge part of an image */ public static boolean imagecopymerge(QuercusImage dest, QuercusImage src, int dx, int dy, int sx, int sy, int w, int h, int pct) { BufferedImage rgba = new BufferedImage(dest.getWidth(), dest.getHeight(), BufferedImage.TYPE_INT_ARGB); rgba.getGraphics().drawImage(src._bufferedImage, 0, 0, null); BufferedImageOp rescaleOp = new RescaleOp(new float[] { 1, 1, 1, ((float)pct) / 100 }, new float[] { 0, 0, 0, 0 }, null); BufferedImage rescaledImage = rescaleOp.filter(rgba, null); Graphics2D g = (Graphics2D)dest.getGraphics().create(); g.setComposite(AlphaComposite.SrcOver); g.drawImage(rescaledImage, dx, dy, dx + w, dy + h, sx, sy, sx + w, sy + h, null); return true; } /** * Copy and merge part of an image with gray scale */ public static boolean imagecopymergegray(QuercusImage dest, QuercusImage src, int dx, int dy, int sx, int sy, int w, int h, int pct) { BufferedImage rgba = new BufferedImage(dest.getWidth(), dest.getHeight(), BufferedImage.TYPE_INT_ARGB); rgba.getGraphics().drawImage(src._bufferedImage, 0, 0, null); BufferedImageOp rescaleOp = new RescaleOp(new float[] { 1, 1, 1, ((float)pct) / 100 }, new float[] { 0, 0, 0, 0 }, null); BufferedImage rescaledImage = rescaleOp.filter(rgba, null); ColorConvertOp colorConvertOp = new ColorConvertOp(ColorSpace.getInstance(ColorSpace.CS_GRAY), null); colorConvertOp.filter(dest._bufferedImage, dest._bufferedImage); Graphics2D g = (Graphics2D)dest.getGraphics().create(); g.setComposite(AlphaComposite.SrcOver); g.drawImage(rescaledImage, dx, dy, dx + w, dy + h, sx, sy, sx + w, sy + h, null); return true; } /** * Copy and resize part of an image with resampling */ public static boolean imagecopyresampled(QuercusImage dest, QuercusImage src, int dx, int dy, int sx, int sy, int dw, int dh, int sw, int sh) { Graphics2D g = (Graphics2D)dest.getGraphics().create(); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g.drawImage(src._bufferedImage, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null); g.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_DEFAULT); return true; } /** * Copy and resize part of an image */ public static boolean imagecopyresized(QuercusImage dest, QuercusImage src, int dx, int dy, int sx, int sy, int dw, int dh, int sw, int sh) { Graphics2D g = (Graphics2D)dest.getGraphics().create(); g.drawImage(src._bufferedImage, dx, dy, dx + dw, dy + dh, sx, sy, sx + sw, sy + sh, null); return true; } /** * Create a new palette based image */ public static Value imagecreate(int width, int height) { QuercusImage image = new QuercusImage(width, height); image.setToFill(true); return image; } /** * Create a new image from GD2 file or URL */ public static void imagecreatefromgd2(Path file) { throw new QuercusException(".gd images are not supported"); } /** * Create a new image from a given part of GD2 file or URL */ public static void imagecreatefromgd2part(Path file, int srcX, int srcY, int width, int height) { throw new QuercusException(".gd images are not supported"); } /** * Create a new image from GD file or URL */ public static void imagecreatefromgd(Path file) { throw new QuercusException(".gd images are not supported"); } /** * Create a new image from file or URL */ public static QuercusImage imagecreatefromgif(Env env, Path filename) { return new QuercusImage(env, filename); } /** * Create a new image from file or URL */ @ReturnNullAsFalse public static QuercusImage imagecreatefromjpeg(Env env, Path filename) { try { return new QuercusImage(env, filename); } catch (Exception e) { env.warning(L.l("Can't open {0} as a jpeg image.\n{1}", filename, e)); log.log(Level.FINE, e.toString(), e); return null; } } /** * Create a new image from file or URL */ public static QuercusImage imagecreatefrompng(Env env, Path filename) { return new QuercusImage(env, filename); } /** * Create a new image from the image stream in the string */ public static QuercusImage imagecreatefromstring(Env env, InputStream data) { if (data == null) return null; return new QuercusImage(data); } /** * Create a new image from file or URL */ public static QuercusImage imagecreatefromwbmp(Env env, Path filename) { return new QuercusImage(env, filename); } /** * Create a new image from file or URL */ public static Value imagecreatefromxbm(Env env, Path filename) { return new QuercusImage(env, filename); } /** * Create a new image from file or URL */ public static QuercusImage imagecreatefromxpm(Env env, Path filename) { return new QuercusImage(env, filename); } /** * Create a new true color image */ public static Value imagecreatetruecolor(int width, int height) { return new QuercusImage(width, height); } /** * Draw a dashed line */ public static boolean imagedashedline(QuercusImage image, int x1, int y1, int x2, int y2, int color) { Graphics2D g = image.getGraphics(); Stroke stroke = g.getStroke(); g.setColor(intToColor(color)); g.setStroke(new BasicStroke(1, BasicStroke.JOIN_ROUND, BasicStroke.CAP_ROUND, 1, new float[] { 5, 5 }, 0)); g.draw(new Line2D.Float(x1, y1, x2, y2)); g.setStroke(stroke); return true; } /** * Destroy an image */ public static boolean imagedestroy(QuercusImage image) { // no-op return true; } /** * Draw an ellipse */ public static boolean imageellipse(QuercusImage image, double cx, double cy, double width, double height, int color) { Shape shape = new Ellipse2D.Double( cx - width / 2, cy - height / 2, width, height); image.stroke(shape, color); return true; } /** * Flood fill */ public static boolean imagefill(QuercusImage image, int x, int y, int color) { image.flood(x, y, color); return true; } /** * Draw a partial ellipse and fill it */ public static boolean imagefilledarc(QuercusImage image, double cx, double cy, double width, double height, double start, double end, int color, int style) { int type = Arc2D.PIE; if ((style & IMG_ARC_CHORD) != 0) type = Arc2D.CHORD; Arc2D arc = new Arc2D.Double(cx - width / 2, cy - height / 2, width, height, -1 * start, -1 * (end - start), type); if ((style & IMG_ARC_NOFILL) == 0) image.fill(arc, color); if ((style & IMG_ARC_EDGED) != 0) image.stroke(arc, color); return true; } /** * Draw a filled ellipse */ public static boolean imagefilledellipse(QuercusImage image, double cx, double cy, double width, double height, int color) { Ellipse2D ellipse = new Ellipse2D.Double(cx - width / 2, cy - height / 2, width, height); image.fill(ellipse, color); return true; } /** * Draw a filled polygon */ public static boolean imagefilledpolygon(Env env, QuercusImage image, ArrayValue points, int numPoints, int color) { image.fill(arrayToPolygon(env, points, numPoints), color); return true; } /** * Draw a filled rectangle */ public static boolean imagefilledrectangle(QuercusImage image, int x1, int y1, int x2, int y2, int color) { image.fill(new Rectangle2D.Float(x1, y1, x2 - x1 + 1, y2 - y1 + 1), color); return true; } /** * Flood fill to specific color */ public static boolean imagefilltoborder(QuercusImage image, int x, int y, int border, int color) { image.flood(x, y, color, border); return true; } // Filters ///////////////////////////////////////////////////////// /** * Applies a filter to an image */ public static boolean imagefilter(Env env, QuercusImage image, int filterType, @Optional int arg1, @Optional int arg2, @Optional int arg3) { switch(filterType) { case IMG_FILTER_NEGATE: // Reverses all colors of the image. env.warning(L.l("imagefilter(IMG_FILTER_NEGATE) unimplemented")); return false; case IMG_FILTER_GRAYSCALE: // Converts the image into grayscale. env.warning(L.l("imagefilter(IMG_FILTER_GRAYSCALE) unimplemented")); return false; case IMG_FILTER_BRIGHTNESS: // Changes brightness of the image. Arg1 sets level of brightness. env.warning(L.l("imagefilter(IMG_FILTER_BRIGHTNESS) unimplementetd")); return false; case IMG_FILTER_CONTRAST: // Changes contrast of the image. Use arg1 to set level of contrast. env.warning(L.l("imagefilter(IMG_FILTER_CONTRAST) unimplementetd")); return false; case IMG_FILTER_COLORIZE: // Like IMG_FILTER_GRAYSCALE, except you can specify the color. Use // arg1, arg2 and arg3 in the form of red, blue, green. The range // for each color is 0 to 255. env.warning(L.l("imagefilter(IMG_FILTER_COLORIZE) unimplemented")); return false; case IMG_FILTER_EDGEDETECT: // Uses edge detection to highlight the edges in the image. env.warning(L.l("imagefilter(IMG_FILTER_EDGEDETECT) unimplemented")); return false; case IMG_FILTER_EMBOSS: // Embosses the image. env.warning(L.l("imagefilter(IMG_FILTER_EMBOSS) unimplemented")); return false; case IMG_FILTER_GAUSSIAN_BLUR: // Blurs the image using the Gaussian method. env.warning(L.l("imagefilter(IMG_FILTER_GAUSSIAN_BLUR) " + "unimplemented")); return false; case IMG_FILTER_SELECTIVE_BLUR: // Blurs the image. env.warning(L.l("imagefilter(IMG_FILTER_SELECTIVE_BLUR) " + "unimplemented")); return false; case IMG_FILTER_MEAN_REMOVAL: // Uses mean removal to achieve a "sketchy" effect. env.warning(L.l("imagefilter(IMG_FILTER_MEAN_REMOVAL) " + "unimplemented")); return false; case IMG_FILTER_SMOOTH: // Makes the image smoother. Use arg1 to set the level of smoothness. env.warning(L.l("imagefilter(IMG_FILTER_SMOOTH) unimplemented")); return false; default: throw new QuercusException("unknown filterType in imagefilter()"); } } /** * Get font height. * * @param font a font previously loaded with {@link #imageloadfont}, * or 1 -5 for built-in fonts */ public static int imagefontheight(int font) { if (font < 1) return 8; else if (font == 1) return 8; else if (font == 2) return 13; else if (font == 3) return 13; else if (font == 4) return 16; else if (font == 5) return 15; else return 15; } /** * Get font width. * * @param font a font previously loaded with {@link #imageloadfont}, * or 1 -5 for built-in fonts */ public static int imagefontwidth(int font) { if (font < 1) return 5; else if (font == 1) return 5; else if (font == 2) return 6; else if (font == 3) return 7; else if (font == 4) return 8; else if (font == 5) return 9; else return 9; } /** * draws a true type font image */ public static Value imageftbbox(Env env, double size, double angle, StringValue fontFile, String text, @Optional ArrayValue extra) { try { QuercusImage image = new QuercusImage(100, 100); Graphics2D g = image.getGraphics(); Font font = image.getTrueTypeFont(env, fontFile); if (font == null) font = image.getFont(1); font = font.deriveFont((float) (size * 96.0 / 72.0)); Font oldFont = g.getFont(); g.setFont(font); Rectangle2D rect = font.getStringBounds(text, g.getFontRenderContext()); int descent = g.getFontMetrics(font).getDescent(); g.setFont(oldFont); double x1 = rect.getX(); double y1 = 0; double x2 = rect.getX() + rect.getWidth(); double y2 = rect.getY() + descent - 1; ArrayValue bbox = new ArrayValueImpl(); bbox.put(LongValue.create(Math.round(x1))); bbox.put(LongValue.create(Math.round(y1))); bbox.put(LongValue.create(Math.round(x2))); bbox.put(LongValue.create(Math.round(y1))); bbox.put(LongValue.create(Math.round(x2))); bbox.put(LongValue.create(Math.round(y2))); bbox.put(LongValue.create(Math.round(x1))); bbox.put(LongValue.create(Math.round(y2))); return bbox; } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); return NullValue.NULL; } } /** * draws a true type font image */ public static Value imagefttext(Env env, @NotNull QuercusImage image, double size, double angle, int x, int y, int color, StringValue fontFile, String text, @Optional ArrayValue extra) { try { Graphics2D g = image.getGraphics(); g.setColor(intToColor(color)); Font font = image.getTrueTypeFont(env, fontFile); if (font == null) font = image.getFont(1); double height = size * 96.0 / 72.0; font = font.deriveFont((float) height); g.setFont(font); Object oldAntiAlias = g.getRenderingHint(RenderingHints.KEY_ANTIALIASING); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); AffineTransform oldTransform = g.getTransform(); if (angle != 0) { g.translate(x, y); g.rotate(- Math.toRadians(angle)); g.drawString(text, 0, 0); } else g.drawString(text, x, y); g.setRenderingHint(RenderingHints.KEY_ANTIALIASING, oldAntiAlias); g.setTransform(oldTransform); // XXX: incorrect ArrayValue value = new ArrayValueImpl(); value = value.append(LongValue.create(x)); value = value.append(LongValue.create(y)); value = value.append(DoubleValue.create(x + text.length() * height)); value = value.append(LongValue.create(y)); value = value.append(DoubleValue.create(x + text.length() * height)); value = value.append(DoubleValue.create(y + height)); value = value.append(DoubleValue.create(x)); value = value.append(DoubleValue.create(y + height)); return value; } catch (Exception e) { log.log(Level.WARNING, e.toString(), e); return NullValue.NULL; } } /** * Apply a gamma correction to a GD image */ public static boolean imagegammacorrect(QuercusImage image, float gammaBefore, float gammaAfter) { // this is a no-op in PHP; apparently the GD library dropped // support for gamma correction between v1.8 and v2.0 return true; } /** * Output GD2 image to browser or file */ public static void imagegd2(QuercusImage image, @Optional Path file) { throw new QuercusException("imagegd2 is not implemented"); } /** * Output GD image to browser or file */ public static void imagegd(QuercusImage image, @Optional Path file) { throw new QuercusException("imagegd is not implemented"); } /** * Output image to browser or file */ public static boolean imagegif(Env env, QuercusImage image, @Optional Path path) { try { if (path != null) { WriteStream os = path.openWrite(); try { ImageIO.write(image._bufferedImage, "gif", os); } finally { os.close(); } } else ImageIO.write(image._bufferedImage, "gif", env.getOut()); return true; } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return false; } } // XXX: imagegrabscreen // XXX: imagegrabwindow /** * Enable or disable interlace */ public static boolean imageinterlace(QuercusImage image, @Optional Boolean enable) { if (enable != null) image.setInterlace(enable); // no-op, can safely ignore (just makes images that load top-down) return true; } /** * Finds whether an image is a truecolor image */ public static boolean imageistruecolor(QuercusImage image) { return true; } /** * Output image to browser or file */ public static boolean imagejpeg(Env env, QuercusImage image, @Optional Path path, @Optional int quality) { try { if (path != null) { WriteStream os = path.openWrite(); try { ImageIO.write(image._bufferedImage, "jpeg", os); } finally { os.close(); } } else ImageIO.write(image._bufferedImage, "jpeg", env.getOut()); return true; } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return false; } } /** * Set the alpha blending flag to use the bundled libgd layering effects */ public static boolean imagelayereffect(QuercusImage image, int effect) { // XXX: there is no documentation for how this function ought to work // http://us3.php.net/manual/en/function.imagelayereffect.php return false; } /** * Draw a line */ public static boolean imageline(QuercusImage image, int x1, int y1, int x2, int y2, int color) { image.stroke(new Line2D.Float(x1, y1, x2, y2), color); return true; } /** * Load a new font */ public static long imageloadfont(Path file) { throw new QuercusException("imageloadfont() not implemented"); } /** * Copy the palette from one image to another */ public static boolean imagepalettecopy(QuercusImage source, QuercusImage dest) { return true; } /** * Output a PNG image to either the browser or a file */ public static boolean imagepng(Env env, QuercusImage image, @Optional Path path) { try { if (path != null) { WriteStream os = path.openWrite(); try { ImageIO.write(image._bufferedImage, "png", os); } finally { os.close(); } } else ImageIO.write(image._bufferedImage, "png", env.getOut()); return true; } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return false; } } /** * Draw a polygon */ public static boolean imagepolygon(Env env, QuercusImage image, ArrayValue points, int numPoints, int color) { image.stroke(arrayToPolygon(env, points, numPoints), color); return true; } /** * Give the bounding box of a text rectangle using PostScript Type1 fonts */ public static ArrayValue imagepsbbox(String text, int font, int size, @Optional int space, @Optional int tightness, @Optional float angle) { throw new QuercusException("imagepsbbox() not implemented"); } /** * Make a copy of an already loaded font for further modification */ public static int imagepscopyfont(Value fontIndex) { throw new QuercusException("imagepscopyfont() not implemented"); } /** * Change the character encoding vector of a font */ public static boolean imagepsencodefont(Value fontIndex, Path encodingFile) { throw new QuercusException("imagepsencodefont() not implemented"); } /** * Extend or condense a font */ public static boolean imagepsextendfont(int fontIndex, float extend) { throw new QuercusException("imagepsextendfont() not implemented"); } /** * Free memory used by a PostScript Type 1 font */ public static boolean imagepsfreefont(Value fontIndex) { throw new QuercusException("imagepsfreefont() not implemented"); } /** * Load a PostScript Type 1 font from file */ public static Value imagepsloadfont(Path fontFile) { throw new QuercusException("imagepsloadfont() not implemented"); } /** * Slant a font */ public static boolean imagepsslantfont(Value fontIndex, float slant) { throw new QuercusException("imagepsslantfont() not implemented"); } /** * To draw a text string over an image using PostScript Type1 fonts */ public static ArrayValue imagepstext(QuercusImage image, String text, Value fontIndex, int size, int fg, int bg, int x, int y, @Optional int space, @Optional int tightness, @Optional float angle, @Optional int antialias_steps) { throw new QuercusException("imagepstext() not implemented"); } /** * Draw a rectangle */ public static boolean imagerectangle(QuercusImage image, int x1, int y1, int x2, int y2, int color) { if (x2 < x1) { int tmp = x1; x1 = x2; x2 = tmp; } if (y2 < y1) { int tmp = y1; y1 = y2; y2 = tmp; } image.stroke(new Rectangle2D.Float(x1, y1, x2 - x1, y2 - y1), color); return true; } /** * Rotate an image with a given angle */ public static boolean imagerotate(QuercusImage image, float angle, int backgroundColor, @Optional int ignoreTransparent) { // this function is broken on most PHP installs: "Note: This // function is only available if PHP is compiled with the bundled // version of the GD library." return false; } /** * Set the flag to save full alpha channel information (as opposed to * single-color transparency) when saving PNG images */ public static boolean imagesavealpha(QuercusImage image, boolean set) { // no-op since we currently only support true-color, full-alpha channel return true; } /** * Set the brush image for line drawing */ public static boolean imagesetbrush(QuercusImage image, QuercusImage brush) { image.setBrush(brush); return true; } /** * Set a single pixel */ public static boolean imagesetpixel(QuercusImage image, int x, int y, int color) { image.setPixel(x, y, color); return true; } /** * Set the style for line drawing */ public static boolean imagesetstyle(Env env, QuercusImage image, ArrayValue style) { image.setStyle(env, style); return true; } /** * Set the thickness for line */ public static boolean imagesetthickness(QuercusImage image, int thickness) { image.setThickness(thickness); return true; } // XXX: imagesettile /** * Draw a string horizontally */ public static boolean imagestring(QuercusImage image, int font, int x, int y, String s, int color) { Graphics2D g = image.getGraphics(); g.setColor(intToColor(color)); Font awtfont = image.getFont(font); int height = image.getGraphics().getFontMetrics(awtfont).getAscent(); g.setFont(awtfont); g.drawString(s, x, y + height); return true; } /** * Draw a string vertically */ public static boolean imagestringup(QuercusImage image, int font, int x, int y, String s, int color) { Graphics2D g = image.getGraphics(); AffineTransform oldTransform = g.getTransform(); g.translate(x, y); // g.rotate(-1 * Math.PI / 2); g.rotate(-1 * Math.PI / 2); g.setColor(intToColor(color)); Font awtfont = image.getFont(font); int height = image.getGraphics().getFontMetrics(awtfont).getAscent(); g.setFont(awtfont); g.drawString(s, 0, 0 + height); g.setTransform(oldTransform); return true; } /** * Returns the width of the image. */ public static int imagesx(@NotNull QuercusImage image) { if (image == null) return 0; return image.getWidth(); } /** * Returns the height of the image. */ public static int imagesy(@NotNull QuercusImage image) { if (image == null) return 0; return image.getHeight(); } /** * general affine transformation */ public static boolean image_transform(QuercusImage image, double m00, double m10, double m01, double m11, double m02, double m12) { if (image == null) return false; AffineTransform transform = new AffineTransform(m00, m10, m01, m11, m02, m12); image.getGraphics().transform(transform); return true; } /** * scaling transformation */ public static boolean image_transform_scale(QuercusImage image, double sx, double sy) { if (image == null) return false; image.getGraphics().scale(sx, sy); return true; } /** * shearing transformation */ public static boolean image_transform_shear(QuercusImage image, double shx, double shy) { if (image == null) return false; image.getGraphics().shear(shx, shy); return true; } /** * translation transformation */ public static boolean image_transform_translate(QuercusImage image, double x, double y) { if (image == null) return false; image.getGraphics().translate(x, y); return true; } /** * draws a true type font image */ public static Value imagettfbbox(Env env, double size, double angle, StringValue fontFile, String text) { return imageftbbox(env, size, angle, fontFile, text, null); } /** * draws a true type font image */ public static Value imagettftext(Env env, @NotNull QuercusImage image, double size, double angle, int x, int y, int color, StringValue fontFile, String text) { return imagefttext(env, image, size, angle, x, y, color, fontFile, text, null); } /** * Returns the imagetypes. */ public static long imagetypes() { return IMG_GIF | IMG_JPG | IMG_PNG; } /** * Output image to browser or file */ public static void imagewbmp(QuercusImage image, @Optional Path filename, @Optional int threshhold) { throw new QuercusException("not supported"); } // XXX: imagexbm /** * Embe into single tags. */ public static boolean iptcembed(String iptcdata, String jpegFileName, @Optional int spool) { throw new QuercusException("iptcembed is not [yet] supported"); } /** * Convert JPEG image file to WBMP image file */ public static void jpeg2wbmp(String jpegFilename, String wbmpName, int d_height, int d_width, int threshhold) { throw new QuercusException("not supported"); } /** * Convert PNG image file to WBM */ public static void png2wbmp(String pngFilename, String wbmpName, int d_height, int d_width, int threshhold) { throw new QuercusException("not supported"); } // Private Helpers //////////////////////////////////////////////////////// private static Polygon arrayToPolygon(Env env, ArrayValue points, int numPoints) { Polygon polygon = new Polygon(); Iterator<Value> iter = points.getValueIterator(env); for (int i = 0; i < numPoints; i++) { int x = iter.next().toInt(); int y = iter.next().toInt(); polygon.addPoint(x, y); } return polygon; } private static Color intToColor(int argb) { // don't forget: PHP alpha channel is only 7 bits int alpha = argb >> 24; alpha <<= 1; alpha |= ((alpha & 0x2) >> 1); // copy bit #2 to LSB return new Color((argb >> 16) & 0xff, (argb >> 8) & 0xff, (argb >> 0) & 0xff, alpha); } /** * Parses the image size from the file. */ private static boolean parseImageSize(ReadStream is, ImageInfo info) throws IOException { int ch; ch = is.read(); if (ch == 137) { // PNG - http://www.libpng.org/pub/png/spec/iso/index-object.html if (is.read() != 'P' || is.read() != 'N' || is.read() != 'G' || is.read() != '\r' || is.read() != '\n' || is.read() != 26 || is.read() != '\n') return false; return parsePNGImageSize(is, info); } else if (ch == 'G') { // GIF if (is.read() != 'I' || is.read() != 'F' || is.read() != '8' || ((ch = is.read()) != '7' && ch != '9') || is.read() != 'a') return false; return parseGIFImageSize(is, info); } else if (ch == 0xff) { // JPEG if (is.read() != 0xd8) return false; return parseJPEGImageSize(is, info); } else return false; } /** * Parses the image size from the PNG file. */ private static boolean parsePNGImageSize(ReadStream is, ImageInfo info) throws IOException { int length; while ((length = readInt(is)) > 0) { int type = readInt(is); if (type == PNG_IHDR) { int width = readInt(is); int height = readInt(is); int depth = is.read() & 0xff; int color = is.read() & 0xff; int compression = is.read() & 0xff; int filter = is.read() & 0xff; int interlace = is.read() & 0xff; info._width = width; info._height = height; info._type = IMAGETYPE_PNG; info._bits = depth; info._mime = "image/png"; return true; } else { for (int i = 0; i < length; i++) { if (is.read() < 0) return false; } } int crc = readInt(is); } return false; } /** * Parses the image size from the PNG file. */ private static boolean parseGIFImageSize(ReadStream is, ImageInfo info) throws IOException { int length; int width = (is.read() & 0xff) + 256 * (is.read() & 0xff); int height = (is.read() & 0xff) + 256 * (is.read() & 0xff); int flags = is.read() & 0xff; info._width = width; info._height = height; info._type = IMAGETYPE_GIF; info._bits = flags & 0x7; info._mime = "image/gif"; return true; } /** * Parses the image size from the PNG file. */ private static boolean parseJPEGImageSize(ReadStream is, ImageInfo info) throws IOException { int ch; while ((ch = is.read()) == 0xff) { ch = is.read(); if (ch == 0xff) { is.unread(); } else if (0xd0 <= ch && ch <= 0xd9) { // rst } else if (0x01 == ch) { // rst } else if (ch == 0xc0) { int len = 256 * is.read() + is.read(); int bits = is.read(); int height = 256 * is.read() + is.read(); int width = 256 * is.read() + is.read(); info._width = width; info._height = height; info._type = IMAGETYPE_JPEG; info._bits = bits; info._mime = "image/jpeg"; return true; } else { int len = 256 * is.read() + is.read(); is.skip(len - 2); } } return false; } private static int pngCode(String code) { return ((code.charAt(0) << 24) | (code.charAt(1) << 16) | (code.charAt(2) << 8) | (code.charAt(3))); } private static int readInt(ReadStream is) throws IOException { return (((is.read() & 0xff) << 24) | ((is.read() & 0xff) << 16) | ((is.read() & 0xff) << 8) | ((is.read() & 0xff))); } // Inner Classes //////////////////////////////////////////////////////// static class ImageInfo { int _width; int _height; int _type; int _bits; String _mime; } public static class QuercusImage extends ResourceValue { private Font []_fontArray = new Font[6]; private int _width; private int _height; BufferedImage _bufferedImage; private Graphics2D _graphics; private boolean _isInterlace; private BufferedImage _brush; private int[] _style; private int _thickness; private boolean _isToFill = false; public QuercusImage(int width, int height) { _width = width; _height = height; _bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); _graphics = (Graphics2D)_bufferedImage.getGraphics(); } public QuercusImage(InputStream inputStream) { try { _bufferedImage = ImageIO.read(inputStream); _width = _bufferedImage.getWidth(null); _height = _bufferedImage.getHeight(null); _graphics = (Graphics2D)_bufferedImage.getGraphics(); } catch (IOException e) { throw new QuercusException(e); } } public QuercusImage(Env env, Path filename) { try { _bufferedImage = ImageIO.read(filename.openRead()); _width = _bufferedImage.getWidth(null); _height = _bufferedImage.getHeight(null); _graphics = (Graphics2D)_bufferedImage.getGraphics(); } catch (IOException e) { throw new QuercusException(e); } } public String toString() { return "resource(Image)"; } public void setInterlace(boolean isInterlace) { _isInterlace = isInterlace; } public boolean isInterlace() { return _isInterlace; } public int getPixel(int x, int y) { return _bufferedImage.getRGB(x, y); } public void setPixel(int x, int y, int color) { _bufferedImage.setRGB(x, y, color); } public Graphics2D getGraphics() { return _graphics; } public Font getFont(int fontIndex) { if (fontIndex < 0) fontIndex = 0; else if (fontIndex > 5) fontIndex = 5; Font font = _fontArray[fontIndex]; if (font == null) { switch (fontIndex) { case 0: case 1: font = new Font("sansserif", 0, 8); break; case 2: font = new Font("sansserif", 0, 10); break; case 3: font = new Font("sansserif", 0, 11); break; case 4: font = new Font("sansserif", 0, 12); break; default: font = new Font("sansserif", 0, 14); break; } _fontArray[fontIndex] = font; } return font; } public Font getTrueTypeFont(Env env, StringValue fontPath) throws FontFormatException, IOException { Font font = _fontMap.get(fontPath); if (font != null) return font; Path path = env.lookupPwd(fontPath); if (path.canRead()) { ReadStream is = path.openRead(); try { font = Font.createFont(Font.TRUETYPE_FONT, is); } finally { is.close(); } _fontMap.put(fontPath, font); return font; } if (fontPath.length() > 0 && fontPath.charAt(0) == '/') return null; StringValue gdFontPathKey = env.createString("GDFONTPATH"); StringValue gdFontPath = OptionsModule.getenv(env, gdFontPathKey).toStringValue(); int start = 0; int len = gdFontPath.length(); while (start < len) { int i = gdFontPath.indexOf(':', start); if (i >= 0 && i + 1 < len && gdFontPath.charAt(i + 1) == ';') { StringValue item = gdFontPath.substring(start, i); path = env.lookupPwd(item); start = i + 2; } else { StringValue item = gdFontPath.substring(start); path = env.lookupPwd(item); start = len; } if (path.canRead()) { ReadStream is = path.openRead(); try { font = Font.createFont(Font.TRUETYPE_FONT, is); } finally { is.close(); } _fontMap.put(fontPath, font); return font; } } return null; } public int getWidth() { return _bufferedImage.getWidth(null); } public int getHeight() { return _bufferedImage.getHeight(null); } public void fill(Shape shape, int color) { _graphics.setColor(intToColor(color)); _graphics.fill(shape); } public void stroke(Shape shape, int color) { switch(color) { case IMG_COLOR_STYLED: strokeStyled(shape); break; case IMG_COLOR_BRUSHED: strokeBrushed(shape); break; default: _graphics.setColor(intToColor(color)); _graphics.setStroke(new BasicStroke(_thickness)); _graphics.draw(shape); break; } } private void strokeStyled(Shape shape) { for (int i = 0; i < _style.length; i++) { _graphics.setColor(intToColor(_style[i])); Stroke stroke = new BasicStroke(_thickness, BasicStroke.JOIN_ROUND, BasicStroke.CAP_ROUND, 1, new float[]{1, _style.length - 1}, i); _graphics.setStroke(stroke); _graphics.draw(shape); } } private void strokeBrushed(Shape shape) { // XXX: support "styled brushes" (see imagesetstyle() example on php.net) Graphics2D g = _graphics; FlatteningPathIterator fpi = new FlatteningPathIterator(shape.getPathIterator(g.getTransform()), 1); float[] floats = new float[6]; fpi.currentSegment(floats); float last_x = floats[0]; float last_y = floats[1]; while (! fpi.isDone()) { fpi.currentSegment(floats); int distance = (int) Math.sqrt( (floats[0] - last_x) * (floats[0] - last_x) + (floats[1] - last_y) * (floats[1] - last_y)); if (distance <= 1) distance = 1; for (int i = 1; i <= distance; i++) { int x = (int) (floats[0] * i + last_x * (distance - i)) / distance; x -= _brush.getWidth() / 2; int y = (int) (floats[1] * i + last_y * (distance - i)) / distance; y -= _brush.getHeight() / 2; g.drawImage(_brush, x, y, null); } last_x = floats[0]; last_y = floats[1]; fpi.next(); } } public void setThickness(int thickness) { _style = null; _thickness = thickness; } public void setStyle(Env env, ArrayValue colors) { _style = new int[colors.getSize()]; Iterator<Value> iter = colors.getValueIterator(env); for (int i = 0; i < _style.length; i++) { _style[i] = iter.next().toInt(); } } public void setBrush(QuercusImage image) { _brush = image._bufferedImage; } public BufferedImage getBrush() { return _brush; } public void setToFill(boolean isToFill) { _isToFill = isToFill; } public long allocateColor(int r, int g, int b) { int color = ((0x7f << 24) | ((r & 0xff) << 16) | ((g & 0xff) << 8) | ((b & 0xff) << 0)); if (_isToFill) { _isToFill = false; flood(0, 0, color); } return color; } public void flood(int x, int y, int color) { flood(x, y, color, 0, false); } public void flood(int x, int y, int color, int border) { flood(x, y, color, border, true); } private void flood( int startx, int starty, int color, int border, boolean useBorder) { java.util.Queue<Integer> xq = new LinkedList<Integer>(); java.util.Queue<Integer> yq = new LinkedList<Integer>(); xq.add(startx); yq.add(starty); color &= 0x00ffffff; border &= 0x00ffffff; int height = getHeight(); while (xq.size() > 0) { int x = xq.poll(); int y = yq.poll(); int p = (getPixel(x, y) & 0x00ffffff); if (useBorder ? (p == border || p == color) : (p != 0)) continue; setPixel(x, y, color); for (int i = x - 1; i >= 0; i--) { p = (getPixel(i, y) & 0x00ffffff); if (useBorder ? (p == border || p == color) : (p != 0)) break; setPixel(i, y, color); if (y + 1 < height) { xq.add(i); yq.add(y + 1); } if (y - 1 >= 0) { xq.add(i); yq.add(y - 1); } } for (int i = x + 1; i < getWidth(); i++) { p = (getPixel(i, y) & 0x00ffffff); if (useBorder ? (p == border || p == color) : (p != 0)) break; setPixel(i, y, color); if (y + 1 < height) { xq.add(i); yq.add(y + 1); } if (y - 1 >= 0) { xq.add(i); yq.add(y - 1); } } if (y + 1 < height) { xq.add(x); yq.add(y + 1); } if (y - 1 >= 0) { xq.add(x); yq.add(y - 1); } } } } }