
import se.datadosen.jalbum.JAFilter;
import se.datadosen.jalbum.ModifiesSize;
import se.datadosen.util.*;
import java.io.*;
import java.net.*;
import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.util.HashMap;
import javax.imageio.*;
import java.awt.Color;
import java.util.*;


                                                                   /**
 * Title:        JAlbum filter to add a variety of borders to images.
 * Copyright:    Copyright (c) 2004
 * @author       Jens Troeger. Slightly modified by David Ekholm
 * @version      0.96
 */


public class XBorderFilter implements JAFilter, ModifiesSize
{
	int counter=0;

  // Implements JAFilter
  public String getName()
  {
    return "eXtended Border filter";
  }

  // Implements JAFilter
  public String getDescription()
  {
    return "Add a variety of borders to images";
  }

  // Implements ModifiesSize
  public Dimension getModifiedSize(Dimension originalSize, Map vars) {
    // first time run initialization
    interpretParams(true);

    // calculate new image size
    int nW = originalSize.width +frameWidthSumE+frameWidthSumW+shadowWidthX+shadowExcessX;
    int nH = originalSize.height+frameWidthSumN+frameWidthSumS+shadowWidthY+shadowExcessY;

    frameWidthSumE = frameWidthSumW  = 0;
    frameWidthSumN = frameWidthSumS  = 0;

    Dimension dim = new Dimension(nW, nH);
	return dim;
  }


  // Implements JavaBean setter
  public void setBoWidth (String  s) { borderWidths=s;   };
  public void setBoWidthN(String  s) { borderWidthsN=s;  };
  public void setBoWidthE(String  s) { borderWidthsE=s;  };
  public void setBoWidthS(String  s) { borderWidthsS=s;  };
  public void setBoWidthW(String  s) { borderWidthsW=s;  };
  public void setBoTrans (String  s) { borderTranss=s;   };
  public void setBoCol   (String  s) { borderColors=s;   };
  public void setBoBlend (boolean b) { borderBlend=b;    };
  public void setBoMargin(int i)     { borderMargin=i;   };
  public void setBoClip  (int i)     { borderClip=i;     };

  public void setFrWidth (String  s) { frameWidths=s;    };
  public void setFrWidthN(String  s) { frameWidthsN=s;   };
  public void setFrWidthE(String  s) { frameWidthsE=s;   };
  public void setFrWidthS(String  s) { frameWidthsS=s;   };
  public void setFrWidthW(String  s) { frameWidthsW=s;   };
  public void setFrTrans (String  s) { frameTranss=s;    };
  public void setFrCol   (String  s) { frameColors=s;    };
  public void setFrBlend (boolean b) { frameBlend=b;     };

  public void setShWidth (int i)     { shadowWidth=i;    };
  public void setShWidthX(int i)     { shadowWidthX=i;   };
  public void setShWidthY(int i)     { shadowWidthY=i;   };
  public void setShExX   (int i)     { shadowExcessX=i;  };
  public void setShExY   (int i)     { shadowExcessY=i;  };
  public void setShCol   (String s)  { shadowColor=Colors.getHTMLColor(s); };
  public void setShTrans (int i)     { shadowTrans=i;    };
  public void setShDir   (String s)
  {
    shadowDir=SOUTH_EAST;
    if (s.equalsIgnoreCase("SOUTH_WEST")) { shadowDir=SOUTH_WEST; };
    if (s.equalsIgnoreCase("SOUTHWEST"))  { shadowDir=SOUTH_WEST; };
    if (s.equalsIgnoreCase("SW"))         { shadowDir=SOUTH_WEST; };
    if (s.equalsIgnoreCase("NORTH_WEST")) { shadowDir=NORTH_WEST; };
    if (s.equalsIgnoreCase("NORTHWEST"))  { shadowDir=NORTH_WEST; };
    if (s.equalsIgnoreCase("NW"))         { shadowDir=NORTH_WEST; };
    if (s.equalsIgnoreCase("NORTH_EAST")) { shadowDir=NORTH_EAST; };
    if (s.equalsIgnoreCase("NORTHEAST"))  { shadowDir=NORTH_EAST; };
    if (s.equalsIgnoreCase("NE"))         { shadowDir=NORTH_EAST; };
  };
  public void setShDrop  (String s)
  {
    shadowDrop=DROP_SOFT;
    if (s.equalsIgnoreCase("SOFT")) { shadowDrop=DROP_SOFT; };
    if (s.equalsIgnoreCase("NORM")) { shadowDrop=DROP_NORM; };
    if (s.equalsIgnoreCase("HARD")) { shadowDrop=DROP_HARD; };
  };

  public void setBgFile  (String s)  { bgFile=s;                       };
  public void setBgXOffset  (int x)  { bgXOffset = x;                  };
  public void setBgYOffset  (int y)  { bgYOffset = y;                  };
  public void setBgCol   (String s)  { bgColor=Colors.getHTMLColor(s); };
  public void setClip    (int i)     { clip=i;                         };

  public void setCacheMax(int i)     { cacheMax=i;           };

  // Implements JavaBean getter
  public String  getBoWidth () { return borderWidths;   };
  public String  getBoWidthN() { return borderWidthsN;  };
  public String  getBoWidthE() { return borderWidthsE;  };
  public String  getBoWidthS() { return borderWidthsS;  };
  public String  getBoWidthW() { return borderWidthsW;  };
  public String  getBoTrans () { return borderTranss;   };
  public String  getBoCol   () { return borderColors;   };
  public boolean getBoBlend () { return borderBlend;    };
  public int     getBoMargin() { return borderMargin;   };
  public int     getBoClip  () { return borderClip;     };

  public String  getFrWidth () { return frameWidths;    };
  public String  getFrWidthN() { return frameWidthsN;   };
  public String  getFrWidthE() { return frameWidthsE;   };
  public String  getFrWidthS() { return frameWidthsS;   };
  public String  getFrWidthW() { return frameWidthsW;   };
  public String  getFrTrans () { return frameTranss;    };
  public String  getFrCol   () { return frameColors;    };
  public boolean getFrBlend () { return frameBlend;     };

  public int     getShWidth () { return shadowWidth;    };
  public int     getShWidthN() { return shadowWidthX;   };
  public int     getShWidthE() { return shadowWidthY;   };
  public int     getShExX   () { return shadowExcessX;  };
  public int     getShExY   () { return shadowExcessY;  };
  public String  getShCol   () { return shadowColor.toString(); };
  public int     getShTrans () { return shadowTrans;    };
  public String  getShDir   ()
  {
    if (shadowDir==SOUTH_WEST) { return "SOUTH_WEST"; };
    if (shadowDir==NORTH_WEST) { return "NORTH_WEST"; };
    if (shadowDir==NORTH_EAST) { return "NORTH_EAST"; };
    return "SOUTH_EAST";
  };
  public String  getShDrop  ()
  {
    if (shadowDrop==DROP_NORM) { return "NORM"; };
    if (shadowDrop==DROP_HARD) { return "HARD"; };
    return "SOFT";
  };
  public String  getBgFile  () { return bgFile;             };
  public int     getBgXOffset () { return bgXOffset;        };
  public int     getBgYOffset () { return bgYOffset;        };
  public String  getBgCol   () { return bgColor.toString(); };
  public int     getClip    () { return clip;               };

  public int     getCacheMax() { return cacheMax;           };

  // implements JAFilter
  // this is the worker method
  public BufferedImage filter(BufferedImage bi, java.util.Map vars)
  {
    //System.out.println("X:" + bgXOffset + " Y:" + bgYOffset);

    // first time run initialization
    if (!paramsInterpreted) interpretParams();

    // all time cache update
    refreshCache(bi.getWidth(),bi.getHeight());

    // put all together
    // calculate new image size
    int nW = bi.getWidth() +frameWidthSumE+frameWidthSumW+shadowWidthX+shadowExcessX;
    int nH = bi.getHeight()+frameWidthSumN+frameWidthSumS+shadowWidthY+shadowExcessY;

    int dW = frameWidthSumW+shadowExcessX;
    int dH = frameWidthSumN+shadowExcessY;

    // create new image
    BufferedImage newImage = new BufferedImage(nW,nH,BufferedImage.TYPE_INT_RGB);
    Graphics2D         g2d = newImage.createGraphics();
    g2d.setComposite(AlphaComposite.SrcOver);

    // background
    g2d.drawRenderedImage(backgroundCache,AffineTransform.getTranslateInstance(0,0));

    // shadow
    if (shadowParts>0)
    {
      g2d.drawRenderedImage(shadowCache,AffineTransform.getTranslateInstance(0,0));
    };

    // original image
    g2d.setClip(new RoundRectangle2D.Double(dW,dH,bi.getWidth(),bi.getHeight(),clip,clip));
    g2d.drawRenderedImage(bi,AffineTransform.getTranslateInstance(dW,dH));
    g2d.setClip(new Rectangle(0,0,nW,nH));

    // frame cache
    if (frameParts>0)
    {
      g2d.drawRenderedImage(frameCache,AffineTransform.getTranslateInstance(shadowExcessX,shadowExcessY));
    };

    // border cache
    if (borderParts>0)
    {
      g2d.drawRenderedImage(borderCache,AffineTransform.getTranslateInstance(dW,dH));
    };

    return newImage;
  }

  // cacheing logic
  // puts the necessary cached building blocks to the caches
  // and builds new blocks if necessary
  private void refreshCache(int w, int h)
  {

//    createBGCache(w,h);	// Offsets may change so refresh

    // each processed image "gets a number"
    lru++;

    // abbreviate, if nothing changed
    //if ((w==lastCacheW) && (h==lastCacheH)) return;

    // we have a new format, note it
    lastCacheW=w; lastCacheH=h;

    // calculate hash to access the cache maps
//    Integer hashKey = new Integer(59365*w+h);
    String hashKey = "" + w + "," + h + "," + bgXOffset + "," + bgYOffset;

    // if old cached entry exist, use them and exit
    if (false && keyMap.containsKey(hashKey))
    {
      // mark the entry as most recently used
      Integer oldLru = (Integer)keyMap.get(hashKey);
      keyMap.remove(hashKey);
      keyMap.put(hashKey, new Integer(lru));

      // mark the entry as most recently used
      lruMap.remove(oldLru);
      lruMap.put(new Integer(lru), hashKey);

      // refresh the cache maps
      backgroundCache = (BufferedImage)backgroundCacheMap.get(hashKey);
      shadowCache     = (BufferedImage)shadowCacheMap.get(hashKey);
      frameCache      = (BufferedImage)frameCacheMap.get(hashKey);
      borderCache     = (BufferedImage)borderCacheMap.get(hashKey);
      return;
    };

    createBGCache(w,h);
    createShadowCache(w,h);
    createFrameCache(w,h);
    createBorderCache(w,h);

    // save them into cache hash map, if possible
    // no save if cacheing is off
    if (cacheMax==0) return;

    // if max entries are yet stored, delete the least recently used one
    if (keyMap.size()==cacheMax)
    {
      // put lrus in a tree and get smallest element (the lru)
      TreeSet  lruTree   = new TreeSet(lruMap.keySet());
      Integer  lruMin    = (Integer)lruTree.first();
      // now get hashKey of this lru
      String  hashMin   = (String)lruMap.get(lruMin);

      // now remove entries
      keyMap.remove(hashMin);
      lruMap.remove(lruMin);
      backgroundCacheMap.remove(hashMin);
      shadowCacheMap.remove(hashMin);
      frameCacheMap.remove(hashMin);
      borderCacheMap.remove(hashMin);
    };

    // here, we have allways at least one entry free
    keyMap.put(hashKey,new Integer(lru));
    lruMap.put(new Integer(lru), hashKey);
    backgroundCacheMap.put(hashKey,backgroundCache);
    shadowCacheMap.put(hashKey,shadowCache);
    frameCacheMap.put(hashKey,frameCache);
    borderCacheMap.put(hashKey,borderCache);
  };

  // create a new cached background
  private void createBGCache(int w, int h)
  {
    int nW = w+frameWidthSumE+frameWidthSumW+shadowWidthX+shadowExcessX;
    int nH = h+frameWidthSumN+frameWidthSumS+shadowWidthY+shadowExcessY;

    backgroundCache = new BufferedImage(nW, nH, BufferedImage.TYPE_INT_RGB);
    Graphics2D  g2d = backgroundCache.createGraphics();

    g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,RenderingHints.VALUE_COLOR_RENDER_QUALITY);

    // set Composite
    g2d.setComposite(AlphaComposite.Src);

    int wF=backgroundFragment.getWidth();
    int hF=backgroundFragment.getHeight();
    for (int i=0; i<=(backgroundCache.getWidth()+bgXOffset)/wF; i++)
    {
      for (int j=0; j<=(backgroundCache.getHeight()+bgYOffset)/hF; j++)
      {
        g2d.drawRenderedImage(backgroundFragment,AffineTransform.getTranslateInstance(i*wF-bgXOffset,j*hF-bgYOffset));
      };
    };
  };

  // create a new cached shadow
  private void createShadowCache(int w, int h)
  {
    int nW = w+frameWidthSumE+frameWidthSumW+shadowWidthX+shadowExcessX;
    int nH = h+frameWidthSumN+frameWidthSumS+shadowWidthY+shadowExcessY;

    int shWmax = max(shadowWidthX, shadowWidthY);
        shWmax = max(shadowExcessX, shWmax);
        shWmax = max(shadowExcessY, shWmax);

    int r  = clip+shWmax;
    if (clip>0)  { r+=frameWidthSum; };

    shadowCache = new BufferedImage(nW, nH, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d = shadowCache.createGraphics();

    // set Composite
    g2d.setStroke(new BasicStroke(2.0f));
    g2d.setComposite(AlphaComposite.Src);

    g2d.setColor(shadowColor);

    float    fac = 0;

    switch (shadowDrop)
    {
      case DROP_SOFT: fac=3.0f/2; break;
      case DROP_NORM: fac=2.0f/2; break;
      case DROP_HARD: fac=4.0f/6; break;
    };

    int trans   = 0;
    int transMax=check(255-255*shadowTrans/100);
    int i;
    for (i=0; i<shWmax; i++)
    {
      Color c= new Color(shadowColor.getRed(), shadowColor.getGreen(), shadowColor.getBlue(), trans);
      g2d.setColor(c);
      g2d.drawRoundRect(i,i,nW-2*i,nH-2*i,r,r);
      trans=java.lang.Math.round((float)(transMax*(java.lang.Math.pow(1.0f*i/shWmax,fac))));
      if (r>0) { r--; };
    };
    g2d.drawRoundRect(i,i,nW-2*i,nH-2*i,r,r);
    Color c= new Color(shadowColor.getRed(), shadowColor.getGreen(), shadowColor.getBlue(), trans);
    g2d.setColor(c);
    g2d.fillRoundRect(i,i,nW-2*i,nH-2*i,r,r);
  };

  // create a new cached frame
  private void createFrameCache(int w, int h)
  {
    int   nW  = w+frameWidthSumW+frameWidthSumE;
    int   nH  = h+frameWidthSumN+frameWidthSumS;

    int   r   = clip;
    float dr  = 0;
    float drS = 0;

    BufferedImage blurred = new BufferedImage(nW, nH, BufferedImage.TYPE_INT_ARGB);
    BufferedImage canvas  = new BufferedImage(nW, nH, BufferedImage.TYPE_INT_ARGB);
    Graphics2D g2d        = canvas.createGraphics();

    g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,RenderingHints.VALUE_COLOR_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL,RenderingHints.VALUE_STROKE_NORMALIZE);

    // clear frame interior
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR,0.0f));
    g2d.fillRect(0,0,nW,nH);

    // set Composite
    g2d.setStroke(new BasicStroke(2.0f));
    g2d.setComposite(AlphaComposite.Src);

    if (r>0)
    {
      r += min(frameWidthSumN,min(frameWidthSumE,min(frameWidthSumS,frameWidthSumW)));
      dr = 1.0f*min(frameWidthSumN,min(frameWidthSumE,min(frameWidthSumS,frameWidthSumW)))/
           max(frameWidthSumN,max(frameWidthSumE,max(frameWidthSumS,frameWidthSumW)));
      drS=r;
    };

    int n0=0,e0=0,s0=0,w0=0;
    shadowClip=0;

    for (int i=0; i<frameParts; i++)
    {
      int   tN = ((Integer)frameWidthN.get(i)).intValue();
      int   tE = ((Integer)frameWidthE.get(i)).intValue();
      int   tS = ((Integer)frameWidthS.get(i)).intValue();
      int   tW = ((Integer)frameWidthW.get(i)).intValue();
      int   tMax=max(tN,max(tE,max(tS,tW)));
      Color c1=(Color)frameColor.get(i);
      Color c2=c1;
      int   tr1=check(255-255*((Integer)frameTrans.get(i)).intValue()/100);
      int   tr2=tr1;
      if (frameBlend)
      {
        c2=(Color)frameColor.get(i+1);
        tr2 = check(255-255*((Integer)frameTrans.get(i+1)).intValue()/100);
      }

      for (int j=0; j<tMax; j++)
      {
        int red  =(tMax-j)*c1.getRed()/tMax  +j*c2.getRed()/tMax;
        int green=(tMax-j)*c1.getGreen()/tMax+j*c2.getGreen()/tMax;
        int blue =(tMax-j)*c1.getBlue()/tMax +j*c2.getBlue()/tMax;
        int trans=check((tMax-j)*tr1/tMax+j*tr2/tMax);
        g2d.setColor(new Color(red,green,blue,trans));

        g2d.drawRoundRect(w0+j*tW/tMax,n0+j*tN/tMax,nW-w0-e0-j*(tW+tE)/tMax-1,nH-n0-s0-j*(tN+tS)/tMax-1,r,r);

        if (r>0) { drS-=dr; shadowClip=r; r=java.lang.Math.round(drS); };
      };
      n0+=tN; e0+=tE; s0+=tS; w0+=tW;
    };

    // clear frame interior
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR,0.0f));
    g2d.fillRoundRect(frameWidthSumW,frameWidthSumN,w,h,clip,clip);

    // put all together
    if (clip>0)
    {
      // smooth frame
      float blur[] = {0.05f,0.05f,0.05f,0.05f,0.6f,0.05f,0.05f,0.05f,0.05f};
      Kernel kernel=new Kernel(3,3,blur);
      ConvolveOp cop=new ConvolveOp(kernel,ConvolveOp.EDGE_NO_OP,null);
      cop.filter(canvas,blurred);
      canvas = blurred;
    }

    frameCache = canvas;
  };

  // create a new cached border
  private void createBorderCache(int w, int h)
  {
    borderCache     = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
    Graphics2D  g2d = borderCache.createGraphics();

    int   r   = borderClip;
    float dr  = 0;
    float drS = 0;

    g2d.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION,RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,RenderingHints.VALUE_ANTIALIAS_ON);
    g2d.setRenderingHint(RenderingHints.KEY_RENDERING,RenderingHints.VALUE_RENDER_QUALITY);
    g2d.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING,RenderingHints.VALUE_COLOR_RENDER_QUALITY);

    // clear border image
    g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.CLEAR,0.0f));
    g2d.fillRect(0,0,w,h);

    // set Composite
    g2d.setStroke(new BasicStroke(2f));
    g2d.setComposite(AlphaComposite.Src);

    if (r>0)
    {
      r += min(borderWidthSumN,min(borderWidthSumE,min(borderWidthSumS,borderWidthSumW)));
      dr = 1.0f*min(borderWidthSumN,min(borderWidthSumE,min(borderWidthSumS,borderWidthSumW)))/
           max(borderWidthSumN,max(borderWidthSumE,max(borderWidthSumS,borderWidthSumW)));
      drS=r;
    };

    int n0=borderMargin,e0=borderMargin,s0=borderMargin,w0=borderMargin;

    for (int i=0; i<borderParts; i++)
    {
      int   tN = ((Integer)borderWidthN.get(i)).intValue();
      int   tE = ((Integer)borderWidthE.get(i)).intValue();
      int   tS = ((Integer)borderWidthS.get(i)).intValue();
      int   tW = ((Integer)borderWidthW.get(i)).intValue();
      int   tMax=max(tN,max(tE,max(tS,tW)));
      Color c1=(Color)borderColor.get(i);
      Color c2=c1;
      int   tr1=check(255-255*((Integer)borderTrans.get(i)).intValue()/100);
      int   tr2=tr1;
      if (borderBlend)
      {
        c2  = (Color)borderColor.get(i+1);
        tr2 = check(255-255*((Integer)borderTrans.get(i+1)).intValue()/100);
      }
      for (int j=0; j<tMax; j++)
      {
        int red  =check((tMax-j)*c1.getRed()/tMax  +j*c2.getRed()/tMax);
        int green=check((tMax-j)*c1.getGreen()/tMax+j*c2.getGreen()/tMax);
        int blue =check((tMax-j)*c1.getBlue()/tMax +j*c2.getBlue()/tMax);
        int trans=check((tMax-j)*tr1/tMax+j*tr2/tMax);
        if (borderClip>0) trans=check(((tMax-j)*tr1/tMax+j*tr2/tMax)/2f);
        g2d.setColor(new Color(red,green,blue,trans));
        g2d.drawRoundRect(w0+j*tW/tMax,n0+j*tN/tMax,w-w0-e0-j*(tW+tE)/tMax-1,h-n0-s0-j*(tN+tS)/tMax-1,r,r);
        if (r>borderClip) { drS-=dr; r=java.lang.Math.round(drS); };
      };
      n0+=tN; e0+=tE; s0+=tS; w0+=tW;
    };
  };



  // parameter interpretion, checking and adjusting
  private void interpretParams() {
	  interpretParams(false);
  }

  // parameter interpretion, checking and adjusting
  private void interpretParams(boolean quick)
  {
    if (!quick) paramsInterpreted=true;

    int             i;
    Integer         trans;
    Color           c;
    StringTokenizer tok;

    // border params

    // boWidth
    borderWidth = new Vector();

    // translate uniform border widths
    tok = new StringTokenizer(borderWidths, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      borderWidth.addElement(width);
      borderWidthN.addElement(width);
      borderWidthE.addElement(width);
      borderWidthS.addElement(width);
      borderWidthW.addElement(width);
      borderWidthSum+=width.intValue();
      borderWidthSumN+=width.intValue();
      borderWidthSumE+=width.intValue();
      borderWidthSumS+=width.intValue();
      borderWidthSumW+=width.intValue();
    }


    // boWidthN: replace values with N border widths
    i=0;
    tok = new StringTokenizer(borderWidthsN, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (borderWidthN.size() <= i) borderWidthN.addElement(width);
      borderWidthSumN-=(((Integer)borderWidthN.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // boWidthE: replace values with E border widths
    i=0;
    tok = new StringTokenizer(borderWidthsE, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (borderWidthE.size() <= i) borderWidthE.addElement(width);
      borderWidthSumE-=(((Integer)borderWidthE.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // boWidthS: replace values with S border widths
    i=0;
    tok = new StringTokenizer(borderWidthsS, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (borderWidthS.size() <= i) borderWidthS.addElement(width);
      borderWidthSumS-=(((Integer)borderWidthS.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // boWidthW: replace values with W border widths
    i=0;
    tok = new StringTokenizer(borderWidthsW, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (borderWidthW.size() <= i) borderWidthW.addElement(width);
      borderWidthSumW-=(((Integer)borderWidthW.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // min of all borderParts is effective borderParts
    borderParts=borderWidth.size();
    borderParts=min(borderParts,borderWidthN.size());
    borderParts=min(borderParts,borderWidthE.size());
    borderParts=min(borderParts,borderWidthS.size());
    borderParts=min(borderParts,borderWidthW.size());


	if (!quick) {
		// boCol
		borderColor=new Vector();
		c = Color.white;

		// translate border colors
		tok = new StringTokenizer(borderColors, ",");
		while (tok.hasMoreTokens())
		{
		  c = Colors.getHTMLColor(tok.nextToken());
		  borderColor.addElement(c);
		}

		// set missing colors
		for (i=borderColor.size(); i<borderParts+1; i++)
		{
		  borderColor.addElement(c);
		};

		// boTrans
		borderTrans=new Vector();

		// translate border transparencies
		tok  =new StringTokenizer(borderTranss, ",");
		trans=new Integer(0);
		while (tok.hasMoreTokens())
		{
		  trans=Integer.valueOf(tok.nextToken());
		  borderTrans.addElement(trans);
		}

		// set missing transparencies
		for (i=borderTrans.size(); i<borderParts+1; i++)
		{
		  borderTrans.addElement(trans);
		};
	}


    // frame params

    // frWidth
    frameWidth   =new Vector();

    // translate unique frame widths
    tok = new StringTokenizer(frameWidths, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      frameWidth.addElement(width);
      frameWidthN.addElement(width);
      frameWidthE.addElement(width);
      frameWidthS.addElement(width);
      frameWidthW.addElement(width);
      frameWidthSum+=width.intValue();
      frameWidthSumN+=width.intValue();
      frameWidthSumE+=width.intValue();
      frameWidthSumS+=width.intValue();
      frameWidthSumW+=width.intValue();
    }

    // frWidthN: replace values with N frame widths
    i=0;
    tok = new StringTokenizer(frameWidthsN, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (frameWidthN.size() <= i) frameWidthN.addElement(width);
      frameWidthSumN-=(((Integer)frameWidthN.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // frWidthE: replace values with E frame widths
    i=0;
    tok = new StringTokenizer(frameWidthsE, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (frameWidthE.size() <= i) frameWidthE.addElement(width);
      frameWidthSumE-=(((Integer)frameWidthE.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // frWidthE: replace values with S frame widths
    i=0;
    tok = new StringTokenizer(frameWidthsS, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (frameWidthS.size() <= i) frameWidthS.addElement(width);
      frameWidthSumS-=(((Integer)frameWidthS.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // frWidthW: replace values with W frame widths
    i=0;
    tok = new StringTokenizer(frameWidthsW, ",");
    while (tok.hasMoreTokens())
    {
      Integer width=Integer.valueOf(tok.nextToken());
      if (frameWidthW.size() <= i) frameWidthW.addElement(width);
      frameWidthSumW-=(((Integer)frameWidthW.set(i,width)).intValue()-width.intValue());
      i++;
    }

    // min of all frameParts is effective frameParts
    frameParts=frameWidth.size();
    frameParts=min(frameParts,frameWidthN.size());
    frameParts=min(frameParts,frameWidthE.size());
    frameParts=min(frameParts,frameWidthS.size());
    frameParts=min(frameParts,frameWidthW.size());

	if (!quick) {
		// frCol
		frameColor=new Vector();
		c = Color.white;

		// translate frame colors
		tok = new StringTokenizer(frameColors, ",");
		while (tok.hasMoreTokens())
		{
		  c = Colors.getHTMLColor(tok.nextToken());
		  frameColor.addElement(c);
		}

		// set missing colors
		for (i=frameColor.size(); i<frameParts+1; i++)
		{
		  frameColor.addElement(c);
		};

		// frTrans
		frameTrans=new Vector();

		// translate frame transparencies
		tok  =new StringTokenizer(frameTranss, ",");
		trans=new Integer(0);
		while (tok.hasMoreTokens())
		{
		  trans=Integer.valueOf(tok.nextToken());
		  frameTrans.addElement(trans);
		}

		// set missing transparencies
		for (i=frameTrans.size(); i<frameParts+1; i++)
		{
		  frameTrans.addElement(trans);
		};

		// shadow params

		// shBGFile & shBGCol
		// try to load image from path
		int           w,h;
		BufferedImage image = null;

		try
		{
		  // Read from a file
		  File file = new File(bgFile);
		  image = ImageIO.read(file);
		}
		// if it doesn't work, try to use it as URL
		catch (IOException e1)
		{
		  try
		  {
			// Read from a URL
			URL  url  = new URL(bgFile);
			image = ImageIO.read(url);
		  }
		  // it was neither a local file nor a URL, use bgCol
		  catch (IOException e2)
		  {
			image = new BufferedImage(100,100, BufferedImage.TYPE_INT_RGB);
			Graphics2D     g2d = image.createGraphics();
			g2d.setComposite(AlphaComposite.SrcOver);
			g2d.setColor(bgColor);
			g2d.fillRect(0,0,100,100);
		  };
		};

		// make background fragment
		w=image.getWidth(); h=image.getHeight();
		backgroundFragment = new BufferedImage(w,h, BufferedImage.TYPE_INT_RGB);
		Graphics2D     g2d = backgroundFragment.createGraphics();
		g2d.setComposite(AlphaComposite.SrcOver);
		g2d.drawRenderedImage(image, AffineTransform.getTranslateInstance(0, 0));
	}


    // shWidth
    // translate shadow width(s)
    if (shadowWidth!=-1)
    {
      if (shadowWidthX==-1) shadowWidthX=shadowWidth;
      if (shadowWidthY==-1) shadowWidthY=shadowWidth;
    };

    shadowWidthX=max(shadowWidthX,0);
    shadowWidthY=max(shadowWidthY,0);

    // shEx
    shadowExcessX=min(shadowWidthX,shadowExcessX);
    shadowExcessY=min(shadowWidthY,shadowExcessY);

    // swap shadow values according to direction
    if (shadowDir == SOUTH_WEST)
    {
      int x=shadowWidthX; shadowWidthX=shadowExcessX; shadowExcessX=x;
    };
    if (shadowDir == NORTH_WEST)
    {
      int x=shadowWidthX; shadowWidthX=shadowExcessX; shadowExcessX=x;
      int y=shadowWidthY; shadowWidthY=shadowExcessY; shadowExcessY=y;
    };
    if (shadowDir == NORTH_EAST)
    {
      int y=shadowWidthY; shadowWidthY=shadowExcessY; shadowExcessY=y;
    };

    if ((shadowWidthX+shadowWidthY+shadowExcessX+shadowExcessY)>0)
    {
      shadowParts=1;
    };

  };

  // helpers
  int  max(int a, int b)  { return java.lang.Math.max(a,b); };
  int  min(int a, int b)  { return java.lang.Math.min(a,b); };
  void swap(int a, int b) { int x=a; a=b; b=x;              };
  int  check(float a)     { return min(max(java.lang.Math.round(a),0),255); };

  // to be up to date
  private final String  versionString      = "XBorderFilter V0.94 - 2004 J.Troeger";

  // constants
  private final int SOUTH_EAST      = 1;
  private final int SOUTH_WEST      = 2;
  private final int NORTH_WEST      = 3;
  private final int NORTH_EAST      = 4;

  private final int DROP_HARD       = 4;
  private final int DROP_NORM       = 3;
  private final int DROP_SOFT       = 2;

  // internals
  private boolean paramsInterpreted = false;

  private int     lru               = 0;
  private int     cacheMax          = 4;


  // cache adds
  private BufferedImage backgroundFragment;
  private int           lastCacheW  = 0;
  private int           lastCacheH  = 0;

  // actual caches
  private BufferedImage backgroundCache;
  private BufferedImage borderCache;
  private BufferedImage frameCache;
  private BufferedImage shadowCache;

  // cache maps
  private HashMap       lruMap             = new HashMap(cacheMax);
  private HashMap       keyMap             = new HashMap(cacheMax);
  private HashMap       backgroundCacheMap = new HashMap(cacheMax);
  private HashMap       borderCacheMap     = new HashMap(cacheMax);
  private HashMap       frameCacheMap      = new HashMap(cacheMax);
  private HashMap       shadowCacheMap     = new HashMap(cacheMax);

  // variables with default values
  // for borders
  private Vector  borderWidth       = new Vector();
  private String  borderWidths      = "";
  private int     borderWidthSum    = 0;
  private Vector  borderWidthN      = new Vector();
  private String  borderWidthsN     = "";
  private int     borderWidthSumN   = 0;
  private Vector  borderWidthE      = new Vector();
  private String  borderWidthsE     = "";
  private int     borderWidthSumE   = 0;
  private Vector  borderWidthS      = new Vector();
  private String  borderWidthsS     = "";
  private int     borderWidthSumS   = 0;
  private Vector  borderWidthW      = new Vector();
  private String  borderWidthsW     = "";
  private int     borderWidthSumW   = 0;

  private Vector  borderColor       = new Vector();
  private String  borderColors      = "white";

  private Vector  borderTrans       = new Vector();
  private String  borderTranss      = "50";

  private int     borderMargin      = 0;
  private boolean borderBlend       = false;
  private int     borderParts       = 0;

  private int     borderClip        = 0;

  // for frames
  private Vector  frameWidth        = new Vector();
  private String  frameWidths       = "";
  private int     frameWidthSum     = 0;
  private Vector  frameWidthN       = new Vector();
  private String  frameWidthsN      = "";
  private int     frameWidthSumN    = 0;
  private Vector  frameWidthE       = new Vector();
  private String  frameWidthsE      = "";
  private int     frameWidthSumE    = 0;
  private Vector  frameWidthS       = new Vector();
  private String  frameWidthsS      = "";
  private int     frameWidthSumS    = 0;
  private Vector  frameWidthW       = new Vector();
  private String  frameWidthsW      = "";
  private int     frameWidthSumW    = 0;

  private Vector  frameColor        = new Vector();
  private String  frameColors       = "white";

  private Vector  frameTrans        = new Vector();
  private String  frameTranss       = "0";

  private int     frameParts        = 0;
  private boolean frameBlend        = false;

  private int     clip              = 0;

  // for shadows
  private int     shadowWidth       = -1;
  private int     shadowWidthX      = -1;
  private int     shadowWidthY      = -1;
  private int     shadowExcessX     = 0;
  private int     shadowExcessY     = 0;

  private String  bgFile            = "";
  private int     bgXOffset         = 0;
  private int     bgYOffset         = 0;
  private Color   bgColor           = Color.white;
  private Color   shadowColor       = Color.gray;
  private int     shadowTrans       = 0;

  private int     shadowDir         = SOUTH_EAST;
  private int     shadowDrop        = DROP_NORM;
  private int     shadowParts       = 0;

  private int     shadowClip        = 0;


};

