
// ImageSFXs.java
// Andrew Davison, April 2005, ad@fivedots.coe.psu.ac.th

/* Používá nejrůznější operace s obrázky k docílení speciálních efektů.

   Tyto metody se volají z objektu typu ImagesTests, obvykle z metody,
   která implementuje řadu změn obrázků (animační efekt), které
   se provádějí v průběhu několika 'tiků' aktualizace/překraslení 
   animačního cyklu.

   Typy speciálních efektů a jejich veřejné metody:

   efekty založené na metodě drawImage():
     * vykreslí obrázek v jiné velikost: drawResizedImage()
     * vrátí převrácený obrázek: getFlippedImage()
     * vykreslí převrácený obrázek: drawVerticalFlip(), drawHorizFlip()
  
   efekt alfa-kompozice
     * vykreslí ztrácející se obrázek: drawFadedImage();

   efekty afinní transformace
     * vrátí otočený obrázek: getRotatedImage()

   konvoluční efekty
     * vykreslí rozmazaný obrázek: drawBlurredImage();
     * vykreslí rozmazaný obrázek se specifikovanou mírou rozmazání: drawBlurredImage()

   efekty využívající vyhledávací tabulky
     * vykreslí červenější obrázek: drawReddenImage() 
                -- existuje verze s operací LookupOp a RescaleOp 

   efekty operace RescaleOp
     * vykreslí světlejší obrázek: drawBrighterImage()
     * vykreslí negativ obrázku: drawNegatedImage()

   efekty operace BandCombineOp
     * vykreslí obrázek se smíchanými barvami: drawMixedColouredImage()

   efekty na úrovni pixelů
     * z některých pixelů obrázku udělá průhledné: eraseImageParts()
     * některé pixely obrázku změní na žlutou a červenou: zapImageParts()
*/

import java.awt.*;
import java.awt.geom.*;
import java.awt.image.*;
import java.awt.color.*;
import java.util.*;


public class ImageSFXs 
{
  // konstanty pro stanovení typu převrácení obrázku
  public static final int VERTICAL_FLIP = 0;
  public static final int HORIZONTAL_FLIP = 1;
  public static final int DOUBLE_FLIP = 2;   // převrátí horizontálně i vertikálně

  private GraphicsConfiguration gc;

  // předdefinované operace s obrázky
  private RescaleOp negOp, negOpTrans;   // negace obrázku
  private ConvolveOp blurOp;    // rozmazání obrázku

  public ImageSFXs()
  {
    // vezmi objekt typu GraphicsConfiguration, aby se obrázky snadno a efektivně kopírovaly  
    GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
    gc = ge.getDefaultScreenDevice().getDefaultConfiguration();

    initEffects();
  }  // konec konstruktoru: ImageSFXs()


  private void initEffects()
  // Vytvoří předem definované operace nad obrázky pro negaci a rozmazání
  {
    // negativ obrázku - vynásob každou hodnotu číslem -1.0 a přičti 255
    negOp = new RescaleOp(-1.0f, 255f, null);

    // negativ pro obrázky s průsvitností
    float[] negFactors = {-1.0f, -1.0f, -1.0f, 1.0f};  // alfa kanál zůstane beze změny
    float[] offsets = {255f, 255f, 255f, 0.0f};
    negOpTrans = new RescaleOp(negFactors, offsets, null);


    // rozmaž obrázek pomocí konvoluční matice
    float ninth = 1.0f / 9.0f;

    float[] blurKernel = {      // 'hello world' obrázkových operací :)
        ninth, ninth, ninth,
        ninth, ninth, ninth,
        ninth, ninth, ninth
    };

    blurOp = new ConvolveOp(
       new Kernel(3, 3, blurKernel), ConvolveOp.EDGE_NO_OP, null);

  }  // konec metody: initEffects()



  // ------------------ efekty založené na metodě drawImage() -----------------
  // změna velikosti, převrácení


  public void drawResizedImage(Graphics2D g2d, BufferedImage im, int x, int y, 
								double widthChange, double heightChange) 
  // vykreslí obrázek s velikostí vynásobenou parametry widthChange a heightChange
  {
    if (im == null) {
      System.out.println("drawResizedImage: vstupní obrázek má hodnotu null");
      return;
    }

    if (widthChange <= 0) {
      System.out.println("width change cannot <= 0");
      widthChange = 1.0;
    }
    if (heightChange <= 0) {
      System.out.println("height change cannot <= 0");
      heightChange = 1.0;
    }

    int destWidth = (int) (im.getWidth() * widthChange);
    int destHeight = (int) (im.getHeight() * heightChange);

    // uprav levou horní souřadnici (x,y) novéh obrázku tak, aby zůstala ve středu
    int destX = x + im.getWidth()/2 - destWidth/2;
    int destY = y + im.getHeight()/2 - destHeight/2;

    g2d.drawImage(im, destX, destY, destWidth, destHeight, null);
  } // konec metody: drawResizedImage()



  // ------------------ převrácení -----------------------

  public BufferedImage getFlippedImage(BufferedImage im, int flipKind)
  /* Vrátí nový obrázek, který bude převrácený horizontálně, vertikálně
     nebo na obě strany. */
  {
    if (im == null) {
      System.out.println("getFlippedImage: vstupní obrázek má hodnotu null");
      return null;
    }

    int imWidth = im.getWidth();
    int imHeight = im.getHeight();
    int transparency = im.getColorModel().getTransparency();

    BufferedImage copy =  gc.createCompatibleImage(imWidth, imHeight, transparency);
    Graphics2D g2d = copy.createGraphics();
    // g2d.setComposite(AlphaComposite.Src);

    // vykresli překlopený obrázek
    renderFlip(g2d, im, imWidth, imHeight, flipKind);
    g2d.dispose();

    return copy; 
  } // konec metody: getFlippedImage()


  private void renderFlip(Graphics2D g2d, BufferedImage im,
                     int imWidth, int imHeight, int flipKind)
  // převrácení provede tak, že metodě drawImage() předá vhodné souřadnice
  {
    if (flipKind == VERTICAL_FLIP)
      g2d.drawImage(im, imWidth, 0,  0, imHeight,
                      0, 0,  imWidth, imHeight, null);
    else if (flipKind == HORIZONTAL_FLIP)
      g2d.drawImage(im, 0, imHeight,  imWidth, 0,
                      0, 0,  imWidth, imHeight, null);
    else    // předpokládáme DOUBLE_FLIP
      g2d.drawImage(im, imWidth, imHeight,  0, 0,
                      0, 0,  imWidth, imHeight, null);

  }  // konec metody: renderFlip()


  public void drawVerticalFlip(Graphics2D g2d, BufferedImage im, int x, int y) 
  // Vykreslí vertikálně převrácený obrázek, aniž by vytvářel nový objekt typu 
  // BufferedImage
  {
    if (im == null) {
      System.out.println("drawVerticalFlip: vstupní obrázek má hodnotu null");
      return;
    }
    int imWidth = im.getWidth();
    int imHeight = im.getHeight();
    g2d.drawImage(im, x+imWidth, y,  x, y+imHeight,
                      0, 0,  imWidth, imHeight, null);
  } // konec metody: drawVerticalFlip()


  public void drawHorizFlip(Graphics2D g2d, BufferedImage im, int x, int y) 
  // Vykreslí horizontálně převrácený obrázek, aniž by vytvářel nový objekt typu 
  // BufferedImage
  {
    if (im == null) {
      System.out.println("drawHorizFlip: vstupní obrázek má hodnotu null");
      return;
    }
    int imWidth = im.getWidth();
    int imHeight = im.getHeight();
    g2d.drawImage(im, x, y+imHeight,  x+imWidth, y,
                      0, 0,  imWidth, imHeight, null);
  } // konec metody: drawHorizFlip()



  // --------------- efekt alfa-kompozice: vytrácení -----------------


  public void drawFadedImage(Graphics2D g2d, BufferedImage im, 
                                   int x, int y, float alpha)
  /* Míru vytracení určuje parametr alpha.
     alpha == 1 znamená plně viditelný, 0 neviditelný. */
  {
    if (im == null) {
      System.out.println("drawFadedImage: vstupní obrázek má hodnotu null");
      return;
    }

    if (alpha < 0.0f) {
      System.out.println("Alpha musí být >= 0.0f; nastavuji na 0.0f");
      alpha = 0.0f;
    }
    else if (alpha > 1.0f) {
      System.out.println("Alpha musí být <= 1.0f; nastavuji na 1.0f");
      alpha = 1.0f;
    }

    Composite c = g2d.getComposite();  // ulož původní směšovač 

    g2d.setComposite( AlphaComposite.getInstance(
                           AlphaComposite.SRC_OVER, alpha)); 
    g2d.drawImage(im, x, y, null);

    g2d.setComposite(c);
      // obnov původní směšovač, aby nedošlo k narušení budoucího vykreslování
  }  // konec metody: drawFadedImage()



  // --------------- efekt využívající afinní transformaci: rotace -----------------


  public BufferedImage getRotatedImage(BufferedImage src, int angle) 
  /* Vytvoří nový objekt typu BufferedImage, otočený o angle stupňů
     ve směru hodinových ručiček.

     Problém je ovšem s ořezáváním na okrajích. Nejjednodušší řešení je
     mít obrázek s dostatkem (průhledného) prostoru po stranách.
  */
  { 
    if (src == null) {
      System.out.println("getRotatedImage: vstupní obrázek má hodnotu null");
      return null;
    }

    int transparency = src.getColorModel().getTransparency();
    BufferedImage dest =  gc.createCompatibleImage(
                              src.getWidth(), src.getHeight(), transparency );
    Graphics2D g2d = dest.createGraphics();

    AffineTransform origAT = g2d.getTransform(); // ulož původní transform
  
    // otoč souřadný systém obrázku dest kolem jeho středu
    AffineTransform rot = new AffineTransform(); 
    rot.rotate( Math.toRadians(angle), src.getWidth()/2, src.getHeight()/2); 
    g2d.transform(rot); 

    g2d.drawImage(src, 0, 0, null);   // zkopíruj do něj obrázek

    g2d.setTransform(origAT);    // obnov původní transformaci
    g2d.dispose();

    return dest; 
  } // konec metody: getRotatedImage()



  // --------------- konvoluční efekt: rozmazání -----------------


  public void drawBlurredImage(Graphics2D g2d, 
                          BufferedImage im, int x, int y)
  // rozmazáváme pevně daným konvolučním jádrem
  { if (im == null) {
      System.out.println("getBlurredImage: vstupní obrázek má hodnotu null");
      return;
    }
    g2d.drawImage(im, blurOp, x, y);   // použij předeme definovanou operaci ConvolveOp
  }  // konec metody: drawBlurredImage()


  public void drawBlurredImage(Graphics2D g2d, 
                          BufferedImage im, int x, int y, int size)
  /* 
     Argument size slouží ke specifikaci jádra size*size pro rozmazání,
     které se vyplní hodnotami 1/(size*size).

     Parametr size by měl být lichý, aby prostřední prvek jádra
     odpovídal souřadnici, která má být rozmazána.

     Čím větší je hodnota tohoto parametru, tím větší bude jádro a 
     tím větší bude rozmazání výsledného obrázku.

     Problémem ovšem je s kraji obrázku, které nakonec zůstanou 
     nepěkně černé, nebo vůběc nerozmazené, což závisí na tom, kterou
     konstantu EDGE_* operace ConvolveOp použijeme.
  */
  { 
    if (im == null) {
      System.out.println("getBlurredImage: vstupní obrázek má hodnotu null");
      return;
    }
    int imWidth = im.getWidth();
    int imHeight = im.getHeight();
    int maxSize = (imWidth > imHeight) ? imWidth : imHeight;
 
    if ((maxSize%2) == 0)  // je-li sudá
      maxSize--;  // úprava na lichou hodnotu

    if ((size%2) == 0) {  // je-li sudá 
      size++;   // úprava na lichou hodnotu
      System.out.println("Jádro pro rozmazání musí mít lichou velikost; přidávám 1, takže velikost = " + size);
    }

    if (size < 3) {
      System.out.println("Minimální velikost jádra pro rozmazání je 3");
      size = 3;
    }
    else if (size > maxSize) {
      System.out.println("Max. velikost jádra pro rozmazání je  " + maxSize);
      size = maxSize;
    }

    // vytvoř jádro pro rozmazání
    int numCoords = size * size;
    float blurFactor = 1.0f / (float) numCoords;

    float[] blurKernel = new float[numCoords];
    for (int i=0; i < numCoords; i++)
      blurKernel[i] = blurFactor;

    ConvolveOp blurringOp = new ConvolveOp(
                 new Kernel(size, size, blurKernel),
                 ConvolveOp.EDGE_NO_OP, null);   // okraje beze změny 
              // ConvolveOp.EDGE_ZERO_FILL, null);   
                                                // okraje jsou vyplněny černou barvou

    g2d.drawImage(im, blurringOp, x, y);
  }  // konec metody: drawBlurredImage() s argumentem size



  // --------------- efekt operace LookupOp: zčervenání -----------------

  /* Existují dvě verze metody drawReddenImage(): jedna používá operaci
     LookupOp a druhé RescaleOp. Funkčně mezi nimi není žádný rozdíl.
  */


  public void drawReddenImage(Graphics2D g2d, BufferedImage im, 
                                       int x, int y, float brightness)
  // používá LookupOp
  /* vykreslí obrázek se zvýšeným červeným a sníženým zeleným a
     modrým nádechem. Žádný alfa-kanál nezůstane beze změn.
  */
  { if (im == null) {
      System.out.println("drawReddenImage: vstupní obrázek má hodnotu null");
      return;
    }

    if (brightness < 0.0f) {
      System.out.println("Brightness must be >= 0.0f; setting to 0.0f");
      brightness = 0.0f;
    }
    // jas může být menší než 1.0 snižujíce tak červený odstín obrázku

    short[] brighten = new short[256];    // pro červený kanál
    short[] lessen = new short[256];      // pro zelený a modrá kanál
    short[] noChange = new short[256];    // pro alfa kanál

    for(int i=0; i < 256; i++) {
      float brightVal = 64.0f + (brightness * i);
      if (brightVal > 255.0f)
        brightVal = 255.0f;
      brighten[i] = (short) brightVal;
      lessen[i] = (short) ((float)i / brightness);
      noChange[i] = (short) i;
   }

    short[][] brightenRed;
    if (hasAlpha(im)) {
      brightenRed = new short[4][];
      brightenRed[0] = brighten;
      brightenRed[1] = lessen;
      brightenRed[2] = lessen;
      brightenRed[3] = noChange;    // bez tohot řádku by operace LookupOp selhala
                                    // že by bug?
    }
    else {  // neprůsvitné
      brightenRed = new short[3][];
      brightenRed[0] = brighten;
      brightenRed[1] = lessen;
      brightenRed[2] = lessen;
    }
    LookupTable table = new ShortLookupTable(0, brightenRed);
    LookupOp brightenRedOp = new LookupOp(table, null);

    g2d.drawImage(im, brightenRedOp, x, y);
  }  // konec metody: drawReddenImage() using LookupOp


/*
  public void drawReddenImage(Graphics2D g2d, BufferedImage im, 
                                    int x, int y, float brightness)
  // používá RescaleOp
  // vykreslí obrázek se zvýšeným červeným a sníženým zeleným a
     modrým nádechem. Žádný alfa-kanál nezůstane beze změn.
  { 
    if (im == null) {
      System.out.println("drawReddenImage: vstupní obrázek má hodnotu null");
      return;
    }

    if (brightness < 0.0f) {
      System.out.println("Parametr brightness musí být >= 0.0f; nastavuji jej na 0.0f");
      brightness = 0.0f;
    }
    // jas může být menší než 1.0 snižujíce tak červený odstín obrázku

    RescaleOp brighterOp;
    if (hasAlpha(im)) {
      float[] scaleFactors = {brightness, 1.0f/brightness, 1.0f/brightness, 1.0f}; 
                // neprováděj změny v alfa kanálu
                // bez hodnoty 1.0f operace RescaleOp selže, že by bug?
      float[] offsets = {64.0f, 0.0f, 0.0f, 0.0f};
      brighterOp = new RescaleOp(scaleFactors, offsets, null);
    }
    else {  // neprůsvitné
      float[] scaleFactors = {brightness, 1.0f/brightness, 1.0f/brightness}; 
      float[] offsets = {64.0f, 0.0f, 0.0f};
      brighterOp = new RescaleOp(scaleFactors, offsets, null);
    }
    g2d.drawImage(im, brighterOp, x, y);
  }  // konec metody: drawReddenImage() using RescaleOp
*/



  // --------------- efekty operace RescaleOp: zesvětlení, negace  ---------------


  public void drawBrighterImage(Graphics2D g2d, BufferedImage im, 
                                      int x, int y, float brightness)
  /* Pomocí operace RescaleOp vykreslí obrázek s upravenou světlostí.
     Žádný alfa-kanál nezůstane beze změn. */
  { 
    if (im == null) {
      System.out.println("drawBrighterImage: vstupní obrázek má hodnotu null");
      return;
    }

    if (brightness < 0.0f) {
      System.out.println("Parametr brightness musí být >= 0.0f; nastavuji jej na 0.5f");
      brightness = 0.5f;
    }
    // jas může být menší než 1,0, což povede ke tlumenějšímu obrázku

    RescaleOp brighterOp;
    if (hasAlpha(im)) {
       float[] scaleFactors = {brightness, brightness, brightness, 1.0f}; 
                // neprováděj změny v alfa kanálu
                // bez hodnoty 1.0f operace RescaleOp selže, že by bug?
       float[] offsets = {0.0f, 0.0f, 0.0f, 0.0f};
       brighterOp = new RescaleOp(scaleFactors, offsets, null);
    }
    else   // neprůsvitné
      brighterOp = new RescaleOp(brightness, 0, null);

    g2d.drawImage(im, brighterOp, x, y);
  }  // konec metody: drawBrighterImage()


  public void drawNegatedImage(Graphics2D g2d, BufferedImage im, int x, int y)
  /* vykreslí obrázek s 255-<barevnými hodnotami> aplikovanými na své RGB-složky.
     Žádný alfa-kanál nezůstane beze změn. */
  { 
    if (im == null) {
      System.out.println("drawNegatedImage: vstupní obrázek má hodnotu null");
      return;
    }

    if (hasAlpha(im))
      g2d.drawImage(im, negOpTrans, x, y);  // použij předdefinovaný objekt typu RescaleOp
    else
      g2d.drawImage(im, negOp, x, y);
  }  // konec metody: drawNegatedImage()



  // ------------------- efekt operace BandCombineOp  -------------------


  public void drawMixedColouredImage(Graphics2D g2d, BufferedImage im, int x, int y)
  /* Smíchá hodnoty zeleného a modrého spektra obrázku. */
  { 
    if (im == null) {
      System.out.println("drawMixedColouredImage: vstupní obrázek má hodnotu null");
      return;
    }

    BandCombineOp changeColoursOp;
    Random r = new Random();

    if (hasAlpha(im)) {
      float[][] colourMatrix = {
         { 1.0f, 0.0f, 0.0f, 0.0f },           // pro nové červené pásmo, beze změn
         { r.nextFloat(), r.nextFloat(), r.nextFloat(), 0.0f },    // nové zelené pásmo
         { r.nextFloat(), r.nextFloat(), r.nextFloat(), 0.0f },    // nové modré pásmo
         { 0.0f, 0.0f, 0.0f, 1.0f} };   // nezměněné alfa pásmo
      changeColoursOp =  new BandCombineOp(colourMatrix, null);
    }
    else {    // neprůsvitné
      float[][] colourMatrix = {
         { 1.0f, 0.0f, 0.0f },           // pro nové červené pásmo, beze změn
         { r.nextFloat(), r.nextFloat(), r.nextFloat() },    // nové zelené pásmo
         { r.nextFloat(), r.nextFloat(), r.nextFloat() }};   // nové modré pásmo

      changeColoursOp =  new BandCombineOp(colourMatrix, null);
    }

    // vytvoř objekt typu Raster pro filtr 
    Raster sourceRaster = im.getRaster();

    WritableRaster destRaster = changeColoursOp.filter(sourceRaster, null);

    // převeď raster na objekt typu BufferedImage
    BufferedImage newIm = new BufferedImage(im.getColorModel(),
                                          destRaster, false, null);

    g2d.drawImage(newIm, x, y, null);
  }  // konec metody: drawMixedColouredImage()



  // ------------------- efekty na úrovni pixelů -------------------
  // mazání, exploze


  public void eraseImageParts(BufferedImage im, int spacing)
  /* Změní některé pixely obrázku na hodnotu 0, čímž se v obrázku s alfa kanálem stanou
     průhledné, v obrázku bez něj černé. */
  {
    if (im == null) {
      System.out.println("eraseImageParts: vstupní obrázek má hodnotu null");
      return;
    }

    int imWidth = im.getWidth();
    int imHeight = im.getHeight();

    int [] pixels = new int[imWidth * imHeight];
    im.getRGB(0, 0, imWidth, imHeight, pixels, 0, imWidth);

    int i = 0;
    while (i < pixels.length) {
      pixels[i] = 0;    // průsvitný pixel (nebo černý, nemá-li alfa kanál)
      i = i + spacing;
    }
  
    im.setRGB(0, 0, imWidth, imHeight, pixels, 0, imWidth);
  }  // konec metody: eraseImageParts()



  public void zapImageParts(BufferedImage im, double likelihood)
  // změní některé pixely obrázku na červené a žluté
  {
    if (im == null) {
      System.out.println("zapImageParts: vstupní obrázek má hodnotu null");
      return;
    }

    if ((likelihood < 0) || (likelihood > 1)) {
      System.out.println("proměnná likelihood musí být v rozmezí 0 až 1");
      likelihood = 0.5;
    }

    int redCol = 0xf90000;      // téměř úplně červená
    int yellowCol = 0xf9fd00;   // mix červené a zelené

    int imWidth = im.getWidth();
    int imHeight = im.getHeight();

    int [] pixels = new int[imWidth * imHeight];
    im.getRGB(0, 0, imWidth, imHeight, pixels, 0, imWidth);

    double rnd;
    for(int i=0; i < pixels.length; i++) {
      rnd = Math.random();
      if (rnd <= likelihood) {
         if (rnd <=  15*likelihood/16 )    // udělej spíše červenou než žlutou
           pixels[i] = pixels[i] | redCol;
         else 
            pixels[i] = pixels[i] | yellowCol;
      }
    }
  
    im.setRGB(0, 0, imWidth, imHeight, pixels, 0, imWidth);
  }  // konec metody: eraseImageParts()



  // --------- užitečné podpůrné metody --------------

  public boolean hasAlpha(BufferedImage im)
  // má obrázek alfa kanál?
  {
    if (im == null)
      return false;

    int transparency = im.getColorModel().getTransparency();

    if ((transparency == Transparency.BITMASK) || 
        (transparency == Transparency.TRANSLUCENT))
      return true;
    else
      return false;
  } // konec metody: hasAlpha()


  public BufferedImage copyImage(BufferedImage src)
  // vytvoří kopii objektu typu BufferedImage
  {
    if (src == null) {
      System.out.println("copyImage: vstupní obrázek má hodnotu null");
      return null;
    }

    int transparency = src.getColorModel().getTransparency();
    BufferedImage copy =  gc.createCompatibleImage(
                              src.getWidth(), src.getHeight(), transparency );
    Graphics2D g2d = copy.createGraphics();

    // zpokíruj obrázek
    g2d.drawImage(src, 0, 0, null);
    g2d.dispose();

    return copy; 
  } // konec metody: copyImage()



  public BufferedImage makeTransImage(BufferedImage src)
  /* Vytvoří kopii objektu typu BufferedImage, ovšem s alfa-kanálem. */
  {
    if (src == null) {
      System.out.println("makeTransImage: vstupní obrázek má hodnotu null");
      return null;
    }

    BufferedImage dest = new BufferedImage( src.getWidth(), src.getHeight(), 
                                  BufferedImage.TYPE_INT_ARGB);  // alfa kanál
    Graphics2D g2d = dest.createGraphics();

    // zpokíruj obrázek
    g2d.drawImage(src, 0, 0, null);
    g2d.dispose();

    return dest; 
  } // konec metody: makeTransImage()

} // konec třídy: ImageSFXs