Java – pokročilá grafika (bitmapový editor 2)

22. září 2003

V tomto článku dokončíme náš bitmapový editor. Přidáme do něho funkce kopírování oblastí obrázku a vylévání barvy. Povíme si něco o filtrech a seznámíme se s užitečnou komponentou pro výběr barvy. Součástí tohoto článku je rovněž zdrojový kód a funkční ukázka.

Kreslíme bez objektů Shape

Pokud by náš editor neuměl pracovat s jinými objekty pro vykreslování než s objekty Shape a dalšími objekty pro určení druhu pera, případně výplně apod., byl by na tom patrně velmi špatně. Nemohl by například poskytovat funkci kopírování různých oblastí obrázku nebo by dokonce neumožňoval ani práci s textem (což stejně neumožňuje, ale nebyl by problém tuto funkci přidat).

Potřebujeme tedy něco, co by zastřešilo vykreslování všeho toho, co nelze definovat pouze objektem Shape. Za tím účelem si definujeme rozhraní Pix, se kterým samozřejmě instance třídy Obrazek a ObrComponent budou muset umět pracovat (což umí):

package bitmapEdit;
public interface Pix {
   public void vykresli(java.awt.Graphics2D g);
}

Jediná metoda rozhraní Pix vykresli(Graphics2D g) nemá za úkol nic jiného, než „se vykreslit“ prostřednictvím objektu Graphics2D. Instance tříd implementujících toto rozhraní pak klidně mohou vykreslovat obrázky nebo text. Stále však zůstává potřeba najít způsob, jak budou tyto objekty vytvářeny v návaznosti na akce uživatele. Jednou možností, jak to udělat, je prostě vkládat tyto objekty do nějakého potomka třídy Nastroj a sladit určité parametry týkající se rozměrů a umístění objektu Pix. Tímto způsobem lze například řešit kopírování vybraných částí obrázku. Pak tu jsou ale nástroje, jako je například vylévání barvy nebo gumování, jejichž vkládání do nástrojů zmíněného typu by nebylo asi nejlepším řešením. Pro druhý zmíněný typ nástrojů si vytvoříme další rozhraní, které nazveme PixNastroj:

package bitmapEdit;
import java.awt.*;
import java.awt.event.*;
public interface PixNastroj extends Pix{
 public void vykresli(Graphics2D g);
/** Reaguje na událost tažení myši.
*/

 public void tazeno(MouseEvent e);
/** Reaguje na událost stisknutí tlačítka myši.
*/

 public void zmacknuto(MouseEvent e);
/** Reaguje na událost puštění tlačítka myši.
*/

 public void uvolneno(MouseEvent e);
/** Vrací pokud možno co nejmenší oblast nutnou
* k překreslení
*/

 public Rectangle dostanMaleBounds();
}

Tužka

Nyní můžeme přejít k vytvoření nejjednodušší třídy implementující rozhraní PixNastroj. Je jí třída Tuzka:

package bitmapEdit;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
public class Tuzka implements PixNastroj{
//využijeme třídu GeneralPath
   private GeneralPath gp = new GeneralPath();
//abychom věděli, co je potřeba překreslovat při tažení
//myší

   private Point p1 = new Point();
   private Point p2 = new Point();
   public Tuzka(){
     gp.moveTo(0,0);
   }
   public void vykresli(Graphics2D g){
     g.draw(gp);
   }
   public void tazeno(MouseEvent e) {
     p2.x = p1.x; p2.y = p1.y;
     p1.x = e.getX(); p1.y = e.getY();
     gp.lineTo(e.getX(), e.getY());
   }
   public void uvolneno(MouseEvent e) {
     p2.x = p1.x; p2.y = p1.y;
     p1.x = e.getX(); p1.y = e.getY();
     gp.lineTo(e.getX(), e.getY());
   }
   public void zmacknuto(MouseEvent e) {
     gp = new GeneralPath();
     p1.x = e.getX();
     p1.y = e.getY();
     gp.moveTo(e.getX(), e.getY());
   }
   public Rectangle dostanMaleBounds() {
     return new Rectangle(Math.min(p1.x,p2.x),
      Math.min(p1.y,p2.y),Math.abs(p1.x-p2.x),
      Math.abs(p1.y-p2.y));
   }
}

Pro úplnost ještě dodám, že by Tuzka klidně mohla být odvozena od třídy Nastroj. Pokud však nepotřebujeme vytvořenou čáru dále upravovat, je implementování rozhraní PixNastroj rozhodně čistějším řešením.

Vyléváme barvu

Prvním krokem, který musíme při vylévání barvy udělat, je nějakým způsobem získat informace o barvách jednotlivých pixelů. Přímo k tomu je určena třída PixelGrabber z balíčku java.awt.image. Pomocí volání její metody grabPixels() lze získat pole reprezentující barvy jednotlivých pixelů.

Jeden z konstruktorů PixelGrabberu, který se pro naše potřeby nejvíce hodí, vypadá takto: public PixelGrabber(Image img, int x, int y, w, int h, int[] pix, int offset, int scansize). Parametr Image img je obrázek, který nás zajímá, parametry x, y, w a h nám umožňují specifikovat, aby byla zpracována pouze určitá část tohoto obrázku, přičemž parametry x, y jsou souřadnice levého horního rohu této oblasti a parametry w, h určují její šířku a výšku. Pole pix slouží k uložení získaných informací. Parametr offset určuje místo v poli pix, kde se má začít s ukládáním. Parametr scansize určuje šířku obrázku v pixelech.

A takto s objektem PixelGrabber pracuje náš program:

private void grabni(){
   sirka = img.getWidth(null);
   vyska = img.getHeight(null);
   pixely = new int[sirka*vyska];
   bpixely = new boolean[sirka*vyska];
//PixelGrabber pg je soukromý atribut
   pg = new PixelGrabber(img, 0,0,sirka, vyska, pixely, 0, sirka);
   pg.setColorModel(ColorModel.getRGBdefault());
   try{
     pg.grabPixels();
   }
   catch (Exception e){}
}

Nyní předpokládejme, že již máme potřebné informace uložené v poli a můžeme tedy přejít k vlastnímu vyplňování. To budeme provádět jako odezvu na uživatelskou akci zmáčknutí tlačítka myši v metodě zmacknuto(MouseEvent e) definované v rozhraní PixNastroj. Asi nejelegantnější by bylo použít rekurzivní algoritmus, zřejmě bychom však místo vybarvené oblasti dostali pouze hlášku o chybě přetečení zásobníku, a to nechceme. Rovněž tak bychom mohli získat objekt Raster daného obrázku a pomocí něj modifikovat jednotlivé pixely. Tím bychom ale přišli nejen o tu částečnou předvídatelnost chování, kterou by měly instance tříd implementující rozhraní pro nástroje poskytovat (totiž že by za vykreslování nebyla odpovědná metoda vykresli), ale i o možnost vylévat barvu podle objektu Paint obsaženého někde jinde (v tomto případě někde jinde znamená v objektu JednoduchyOvladac). To by mimo jiné znamenalo, že bychom si mohli nechat zdát například o vylévání barevných přechodů atp. Metodu zmacknuto třídy Plechovka (ano, tak se dotyčná třída jmenuje), která hledá všechny body, jež je nutné vykreslit, ukazuje následující výpis:

public void zmacknuto(MouseEvent e) {
//sem budeme ukládat souřadnice naposledy vybarvených bodů
   ArrayList a = new ArrayList();
   ArrayList a2 = new ArrayList();
   Point p;
   if (pixely == null)
     return;
//souřadnice, odkud začneme
   x = e.getX();
   y = e.getY();
/** souřadnice levého horního a pravého dolního rohu
* obdélníku, které slouží k optimalizaci výkonu
* metody vykresli(Graphics2D g)
*/

   xmin = xmax = x;
   ymin = ymax = y;
//barva bodu na souřadnicích x, y
   int c = dostanBarvuBodu(x, y);
/** volá metodu, která nastaví barvu tohoto bodu
* na maskovací hodnotu
*/

   nastavBodNaMasku(x, y);
   a.add(new Point(x,y));
/** cyklem budeme procházet, dokud bueme mít v objektu ArrayList a
* nějaké položky, určující body vybarvené v minulém průchodu cyklem,
* resp. na začátku volání této metody
*/

   while (a.size()>0){
     a2 = a;
     a = new ArrayList();
/** v tomto cyklu projdeme všechny body ležící
* nalevo, napravo, nahoře a dole od každého bodu
* vybarveného v předešlém průchodu cyklem
*/

     for (int i = 0; i < a2.size(); i++){
       p = (Point)a2.get(i);
//procházení bodů vlevo, vpravo atd.
      for (int j = 1; j<8; j+=2){
       int jx, jy;
       jx = j%3-1;
       jy = j/3-1;
       if (p.x+jx < sirka && p.x+jx >= 0 && p.y+jy < vyska &&
        p.y+jy >= 0 && dostanBarvuBodu(p.x+jx, p.y+jy) == c){
        a.add(new Point(p.x+jx, p.y+jy));
        nastavBodNaMasku(p.x+jx, p.y+jy);
        xmin = Math.min(xmin, p.x+jx);
        ymin = Math.min(ymin, p.y+jy);
        xmax = Math.max(xmax, p.x+jx);
        ymax = Math.max(ymax, p.y+jy);
       }
      }
    }
   }
}

Použitý algoritmus je velice primitivní a zcela postrádá optimalizaci výkonu. Nejprve zjistí barvu pixelu na souřadnicích získaných voláním metody getPoint() objektu MouseEvent, pak ji uloží do objektu ArrayList, což je nesynchronizovaná obdoba Vectoru (to kvůli zvýšení výkonu). Potom prochází v cyklu while vždy všechny body ležící nalevo, napravo, nahoře a dole od bodů uložených v ArrayListu. Pokud mají tyto body odpovídající barvu, přidá je do druhého ArrayListu a voláním metody nastavBodNaMasku(int x, int y) označí tyto body určitým způsobem – mimo jiné tak, aby tomu rozuměla metoda vykresli(Graphics2D g). Při dalším průchodu cyklem prohodí ArrayListy, tedy přesněji odkazy na ně, a smaže ArrayList, jehož body byly procházeny v předešlém průchodu. Tak to dělá do té doby, dokud v průchodu cyklem nevybarví žádný bod, což znamená, že už jsou všechny vybarveny. A to je celé kouzlo vylévání barvy.

S vyléváním barvy lze vytvořit například takovouto postavičku

Kopírujeme

Vytvořit nástroj pro kopírování obrázků není nikterak obtížné. Stačí vytvořit nového potomka třídy Obdelnik, který bude ve svém objektu Vykreslovane vracet instanci nějaké třídy implementující rozhraní Pix (v tomto případě se jmenuje Obrazecek), která bude daný obrázek zapouzdřovat. Jak snadné to je, je nejlépe vidět na tom, jak krátký je zdrojový kód těchto tříd.

Trida Vystrizek:

package bitmapEdit;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
public class Vystrizek extends Obdelnik {
   private Obrazecek obr;
   private Vykreslovane v = new Vykreslovane();
   public Vystrizek(Image image) {
     obr = new Obrazecek(image, this);
     v.pridej(obr);
   }
   public Vykreslovane dostanVykreslovane(){
     v.nastavNaZacatek();
     return v;
   }
}

Trida Obrazecek:

package bitmapEdit;
import java.awt.*;
import java.awt.geom.*;
import java.awt.event.*;
public class Obrazecek implements Pix{
   private Image img;
   private Obdelnikovy ob;
   public Obrazecek(Image image, Obdelnikovy o) {
     img = image;
     ob = o;
   }
   public void vykresli(Graphics2D g){
     Point2D.Float p1 = ob.dostanBod(0);
     Point2D.Float p2 = ob.dostanBod(2);
     g.drawImage(img,(int)Math.min(p1.x,p2.x),
      (int)Math.min(p1.y, p2.y),
      (int)Math.abs(p2.x-p1.x),
      (int)Math.abs(p2.y-p1.y),null);
   }
}

Problém tedy nespočívá ve vytvoření nástroje pro vkládání obrázků. Problémem není ani získání požadovaného obdélníkového obrázku, k čemuž ostatně stačí volat metodu getSubimage(int x, int y, int width, int height) objektu BufferedImage a pak získaný obrázek nějak zkopírovat, abychom se vyhnuli nežádoucím efektům způsobených tím, že takto získaný obrázek sdílí svá data s původním obrázkem. O něco větší problémy na nás nicméně čekají při kopírování nepravoúhlých oblastí. Musíme totiž vyřešit otázku, jak z jinak pravoúhlého obrázku vytvořit obrázek takřka libovolného tvaru. Řešením tohoto problému je použití průhlednosti (nebo chcete-li alfa transparence).

Přistoupíme tedy k vytvoření metody dostanVyber() ve třídě JednoduchyOvladac, která bude vracet objekt ImageProducer, jenž pak může být použit jako zdroj dat pro vytvoření požadovaného částečně průhledného obrázku:

public ImageProducer dostanVyber(){
//zjistíme, zda je vůbec něco vybráno
   if ((co == VYBER || co == EDITVYBER) && go != null){
     BufferedImage i2 = obr.vyrizni(go.dostanTvar());
     if (i2 == null)
       return null;
//obrázek, který využijeme k vytvoření masky
     BufferedImage i = new BufferedImage(r.width,r.height,
     BufferedImage.TYPE_4BYTE_ABGR);
//tvar obrázku
     Shape s = go.dostanTvar();
     Graphics2D g = i.createGraphics();
//musíme strefit masku do obrázku
     g.translate(-s.getBounds().x,-s.getBounds().y);
     g.scale(i.getWidth()/s.getBounds().width,i.getHeight()/s.getBounds().height);
     g.setColor(Color.blue);
     g.fill(s);
     g.dispose();
/** vytvoříme filtr, který bude podle masky filtrovat
* obrázek tak, aby neprůhledná byla pouze část, která má
* v maskovacím obrázku barvu předanou jako parametr,
* tedy v tomto případě modrou
*/

     PruhlednostFilter pf = new PruhlednostFilter(Color.blue.getRGB(), i);
//vytvoření zdroje dat pomocí filtru
     FilteredImageSource fip = new FilteredImageSource(i2.getSource(), pf);
       return fip;
     }
   return null;
}

Vlastní nastavení toho, co bude nakonec vykresleno jako neprůhledné a bude tedy vidět, a co naopak bude vykresleno průhledně a vidět tedy nebude, se provádí v objektu PruhlednostFilter. Ten funguje tak, že u všech pixelů, které mají barvu rovnou barvě masky, nastaví v cílovém obrázku složku alfa na 255 (neprůhledné, viditelné) a u ostatních pixelů na 0 (zcela průhledné, neviditelné).

package bitmapEdit;
import java.awt.image.*;
import java.awt.*;
public class PruhlednostFilter extends RGBImageFilter{
   private int m;
   private PixelGrabber pg;
   private int sirka, vyska;
   private int pixely[];
   public PruhlednostFilter(int maska, Image img) {
     m = maska;
     sirka = img.getWidth(null);
     vyska = img.getHeight(null);
     pixely = new int[sirka*vyska];
//získáváme informace o masce
     pg = new PixelGrabber(img, 0,0,sirka, vyska, pixely, 0, sirka);
     try{
       pg.grabPixels();
   }
     catch (Exception e){}
   }
   public int filterRGB(int x, int y, int barva) {
/** jednotlivé bajty v čísle int jsou složky barvy argb:
* alfa, červená, zelená, modrá
*/

     int a = barva & 0xFF000000;
     barva = barva & 0x00FFFFFF;
     if (m == pixely[x+y*sirka])
      a = a & 0xFF000000;
     else
      a = a & 0x00000000;
     return a | barva;
   }
}

Třída PruhlednostFilter je potomkem třídy RGBImageFilter, což je třída, která se používá k vytváření filtrů, které ke své práci potřebují znát pouze barvu a umístění vždy pouze jednoho právě filtrovaného bodu. Třída RGBImageFilter se dá použít mimo jiné k vytvoření filtrů pro změnu světlosti, odstínu nebo nějakého filtru využívajícího masky, jako je například náš PruhlednostFilter. Její metoda filterRGB, která vrací číslo typu int reprezentující novou barvu pixelu (AlphaRedGreenBlue), je volána při použití filtru pro každý bod obrázku.

Vybíráme si barvu

V našem bitmapovém editoru bychom měli uživatelům poskytnout možnost vybrat si barvu podle jejich gusta. Nabízet jen pár předdefinovaných barev by nebylo slušné, vytvářet celé rozhraní pro pohodlný výběr barev, jaký dnes má každý program včetně windowsovského Malování, by zase bylo poměrně pracné. Chce to tedy trochu zapátrat po balíčcích Javy, jestli se tam náhodou neskrývá nějaká komponenta k tomuto určená. A ejhle, skrývá – a hned v balíčku javax.swing. Jmenuje se JColorChooser.

Panel pro výběr barvy

JColorChooser vytvoříme velice snadno voláním konstruktoru JColorChooser(Color c). Zobrazíme jej úplně stejně jako každou jinou grafickou komponentu – zavoláme metodu add objektu Container, do něhož ji chceme vložit. Informace o vybrané barvě se pak dají získat prostřednictvím posluchače ChangeListener, jenž definuje jedinou metodu stateChanged(ChangeEvent e), která je volána vždy, když je vybrána nová barva. Ukázku použití JcolorChooseru v našem programu můžete vidět na výpisu:

Container pane = getContentPane();
pane.setLayout(new BorderLayout());
JButton jb = new JButton(„OK“);
jcc = new JColorChooser(Color.black);
cc.getSelectionModel().addChangeListener(new ChangeListener(){
  public void stateChanged(ChangeEvent e){
    nastavBarvu(e);
  }
});
pane.add(jcc, BorderLayout.CENTER);

Samotný prográmek má samozřejmě mnohem delší kód, než co zde bylo možné ukázat, a některé jeho části jsou docela odbyté, jako například GUI. Rovněž neposkytuje některé nástroje v takovémto typu programu zcela nepostradatelné, jako je například práce s textem nebo vkládání obrázků ze souboru. Není však žádný problém tyto nedostatky odstranit. Objektová struktura tohoto programu, byť je nedokonalá, umožňuje docela snadné vytváření dalších nástrojů. I když se to nezdá, není problémem ani vytvoření nástroje umožňujícího rotaci všech nástrojů odvozených od třídy Nastroj. (Stačí vytvořit další podtřídu třídy Nastroj, která by byla schopná převzít jakýkoliv objekt Nastroj a pracovat s ním.)

Pokud se rozhodnete prográmek stáhnout a vyzkoušet, musím vás upozornit ještě na několik úskalí při práci s ním. Především není implementována podpora klávesových zkratek, chybí spolupráce se systémovou schránkou, akce UNDO a REDO a mnoho dalšího, co by u skutečného programu nemělo nikdy scházet, takže to vypadá, že pozice windowsovského Malování zůstane neotřesena.

Přílohy ke stažení

Štítky: Články

Mohlo by vás také zajímat

Nejnovější

Napsat komentář

Vaše e-mailová adresa nebude zveřejněna. Vyžadované informace jsou označeny *