001/* 002Copyright 2006 Jerry Huxtable 003 004Licensed under the Apache License, Version 2.0 (the "License"); 005you may not use this file except in compliance with the License. 006You may obtain a copy of the License at 007 008 http://www.apache.org/licenses/LICENSE-2.0 009 010Unless required by applicable law or agreed to in writing, software 011distributed under the License is distributed on an "AS IS" BASIS, 012WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013See the License for the specific language governing permissions and 014limitations under the License. 015*/ 016 017package com.jhlabs.image; 018 019import java.awt.*; 020import java.awt.geom.*; 021import java.awt.image.*; 022 023/** 024 * A Filter to pixellate images. 025 */ 026public class ColorHalftoneFilter extends AbstractBufferedImageOp { 027 028 private float dotRadius = 2; 029 private float cyanScreenAngle = (float)Math.toRadians( 108 ); 030 private float magentaScreenAngle = (float)Math.toRadians( 162 ); 031 private float yellowScreenAngle = (float)Math.toRadians( 90 ); 032 033 public ColorHalftoneFilter() { 034 } 035 036 /** 037 * Set the pixel block size. 038 * @param dotRadius the number of pixels along each block edge 039 * @min-value 1 040 * @max-value 100+ 041 * @see #getdotRadius 042 */ 043 public void setdotRadius( float dotRadius ) { 044 this.dotRadius = dotRadius; 045 } 046 047 /** 048 * Get the pixel block size. 049 * @return the number of pixels along each block edge 050 * @see #setdotRadius 051 */ 052 public float getdotRadius() { 053 return dotRadius; 054 } 055 056 /** 057 * Get the cyan screen angle. 058 * @return the cyan screen angle (in radians) 059 * @see #setCyanScreenAngle 060 */ 061 public float getCyanScreenAngle() { 062 return cyanScreenAngle; 063 } 064 065 /** 066 * Set the cyan screen angle. 067 * @param cyanScreenAngle the cyan screen angle (in radians) 068 * @see #getCyanScreenAngle 069 */ 070 public void setCyanScreenAngle( float cyanScreenAngle ) { 071 this.cyanScreenAngle = cyanScreenAngle; 072 } 073 074 /** 075 * Get the magenta screen angle. 076 * @return the magenta screen angle (in radians) 077 * @see #setMagentaScreenAngle 078 */ 079 public float getMagentaScreenAngle() { 080 return magentaScreenAngle; 081 } 082 083 /** 084 * Set the magenta screen angle. 085 * @param magentaScreenAngle the magenta screen angle (in radians) 086 * @see #getMagentaScreenAngle 087 */ 088 public void setMagentaScreenAngle( float magentaScreenAngle ) { 089 this.magentaScreenAngle = magentaScreenAngle; 090 } 091 092 /** 093 * Get the yellow screen angle. 094 * @return the yellow screen angle (in radians) 095 * @see #setYellowScreenAngle 096 */ 097 public float getYellowScreenAngle() { 098 return yellowScreenAngle; 099 } 100 101 /** 102 * Set the yellow screen angle. 103 * @param yellowScreenAngle the yellow screen angle (in radians) 104 * @see #getYellowScreenAngle 105 */ 106 public void setYellowScreenAngle( float yellowScreenAngle ) { 107 this.yellowScreenAngle = yellowScreenAngle; 108 } 109 110 public BufferedImage filter( BufferedImage src, BufferedImage dst ) { 111 int width = src.getWidth(); 112 int height = src.getHeight(); 113 int type = src.getType(); 114 WritableRaster srcRaster = src.getRaster(); 115 116 if ( dst == null ) 117 dst = createCompatibleDestImage( src, null ); 118 119 float gridSize = 2*dotRadius*1.414f; 120 float[] angles = { cyanScreenAngle, magentaScreenAngle, yellowScreenAngle }; 121 float[] mx = new float[] { 0, -1, 1, 0, 0 }; 122 float[] my = new float[] { 0, 0, 0, -1, 1 }; 123 float halfGridSize = (float)gridSize/2; 124 int[] outPixels = new int[width]; 125 int[] inPixels = getRGB( src, 0, 0, width, height, null ); 126 for ( int y = 0; y < height; y++ ) { 127 for ( int x = 0, ix = y*width; x < width; x++, ix++ ) 128 outPixels[x] = (inPixels[ix] & 0xff000000) | 0xffffff; 129 for ( int channel = 0; channel < 3; channel++ ) { 130 int shift = 16-8*channel; 131 int mask = 0x000000ff << shift; 132 float angle = angles[channel]; 133 float sin = (float)Math.sin( angle ); 134 float cos = (float)Math.cos( angle ); 135 136 for ( int x = 0; x < width; x++ ) { 137 // Transform x,y into halftone screen coordinate space 138 float tx = x*cos + y*sin; 139 float ty = -x*sin + y*cos; 140 141 // Find the nearest grid point 142 tx = tx-ImageMath.mod( tx-halfGridSize, gridSize )+halfGridSize; 143 ty = ty-ImageMath.mod( ty-halfGridSize, gridSize )+halfGridSize; 144 145 float f = 1; 146 147 // TODO: Efficiency warning: Because the dots overlap, we need to check neighbouring grid squares. 148 // We check all four neighbours, but in practice only one can ever overlap any given point. 149 for ( int i = 0; i < 5; i++ ) { 150 // Find neigbouring grid point 151 float ttx = tx + mx[i]*gridSize; 152 float tty = ty + my[i]*gridSize; 153 // Transform back into image space 154 float ntx = ttx*cos - tty*sin; 155 float nty = ttx*sin + tty*cos; 156 // Clamp to the image 157 int nx = ImageMath.clamp( (int)ntx, 0, width-1 ); 158 int ny = ImageMath.clamp( (int)nty, 0, height-1 ); 159 int argb = inPixels[ny*width+nx]; 160 int nr = (argb >> shift) & 0xff; 161 float l = nr/255.0f; 162 l = 1-l*l; 163 l *= halfGridSize*1.414; 164 float dx = x-ntx; 165 float dy = y-nty; 166 float dx2 = dx*dx; 167 float dy2 = dy*dy; 168 float R = (float)Math.sqrt( dx2+dy2 ); 169 float f2 = 1-ImageMath.smoothStep( R, R+1, l ); 170 f = Math.min( f, f2 ); 171 } 172 173 int v = (int)(255 * f); 174 v <<= shift; 175 v ^= ~mask; 176 v |= 0xff000000; 177 outPixels[x] &= v; 178 } 179 } 180 setRGB( dst, 0, y, width, 1, outPixels ); 181 } 182 183 return dst; 184 } 185 186 public String toString() { 187 return "Pixellate/Color Halftone..."; 188 } 189} 190