24.5. Przekształcanie obrazków
Kolejnym przykładem jest język pozwalający manipulować obrazkami graficznymi lub zdjęciami. W tym języku można wyrazić takie operacje, jak obcięcie fragmentów obrazka, przycięcie obrazka do określonego formatu lub obrót obrazka o określony kąt. Najpierw zostanie zaprezentowany sam język, a potem jego implementacja. Załóżmy, że mamy zdjęcie, takie jak pokazane na rysunku 24.1, zapisane w pliku o nazwie casabattlo.jpg, które chcemy poddać obróbce. Skrypt CasaBattloScript.scala tworzy na podstawie tego zdjęcia plik casabattlo1.jpg, którego zawartość jest przedstawiona na rysunku 24.2.
Plik CasaBattloScript.scala: import images._
import language.reflectiveCalls (LoadImage fromDir "." fromFile "casabattlo.jpg"
rotate 5
crop 40 px fromTop
crop 40 percent fromBottom
crop 10 percent fromRight
formatTo Format10x15 removing fromLeft or fromTop
saveAs "casabattlo1.jpg")
![]()
Wiersz
skryptu powoduje zaimportowanie składowych pakietu images,
w tym obiektu LoadImage. Począwszy od drugiego wiersza rozpoczyna
się ciąg instrukcji napisanych w zdefiniowanym języku. Cały ciąg jest
ujęty w nawiasy okrągłe, aby zabezpieczyć się przed zinterpretowaniem
znaków końca wiersza jako separatora oddzielającego od siebie
poszczególne wyrażenia.

Wiersz
nakazuje załadowanie danych z pliku casabattlo.jpg z
bieżącego katalogu. Wiersz
nakazuje obrócić obrazek o 5 stopni,
zgodnie z ruchem wskazówek zegara. Wiersze
,
i
nakazują obciąć z
tak powstałego obrazka 40 pikseli od góry, 40 procent bieżącej
wysokości obrazka od dołu oraz 10 procent szerokości obrazka z prawej
strony. Wiersz
nakazuje obcięcie obrazka w taki sposób, aby wynikowy
obrazek miał jeden z uprzednio zdefiniowanych formatów — w tym
przypadku 10 na 15 — przy czym przy określaniu formatu ważny jest
jedynie stosunek obu wartości. Format 10 na 15 oznacza, że wysokość
obrazka ma być o połowę dłuższa od jego szerokości. Wiersz
zawiera
poza tym informację, że zbędne fragmenty obrazka mogą zostać usunięte
z jego lewej albo górnej strony. Wreszcie w wierszu
znajduje się
instrukcja nakazująca zapisanie wynikowego obrazka w pliku
casabattlo1.jpg.
Składnia użytego języka DSL zostanie przedstawiona z użyciem rozszerzonej notacji Backusa-Naura.
dsl = ładowanie obrazka, { transformacja obrazka }, [ zapis obrazka ]
Wyrażenie języka składa się z instrukcji służących do załadowania danych z pliku — ładowanie obrazka — po którym następuje (potencjalnie pusta) sekwencja instrukcji służących do przekształcania danych na inną postać — tranformacja obrazka. Na końcu występuje opcjonalna instrukcja zapisu danych wynikowych do pliku — zapis obrazka.
ładowanie obrazka = "LoadImage", [ "fromDir" katalog ], "fromFile" plik
Wyrażenie ładowanie obrazka, służące do załadowania pliku, rozpoczyna się od słowa kluczowego LoadImage, po którym występuje opcjonalne wyrażenie fromDir katalog, określające katalog, z którego ma być załadowany plik. Wreszcie po nim występuje wyrażenie fromFile plik, określające plik do wczytania.

transformacja obrazka = obrót | przycięcie | formatowanie
Wyrażenie transformacja obrazka jest alternatywą obejmującą trzy inne wyrażenia: obrót, przycięcie oraz formatowanie.
obrót = "rotate", n
Wyrażenie obrót służy do obrócenia obrazka o podaną liczbę stopni zgodnie z kierunkiem ruchu wskazówek zegara. Osią obrotu jest środek obrazka.
przycięcie = "crop", n, jednostka, kierunek jednostka = "px" | "percent" kierunek = kierunek pionowy | kierunek poziomy kierunek pionowy = "fromTop" | "fromBottom" kierunek poziomy = "fromLeft" | "fromRight"
Wyrażenie przycięcie służy do obcięcia części obrazka z jednego z jego boków. Składa się z czterech części. Pierwszą jest słowo kluczowe crop. Drugą jest liczba całkowita n określająca jak duża część obrazka ma być ucięta. Trzecią jest jednostka miary dotycząca liczby n. Może to być albo słowo kluczowe px, co oznacza piksele, albo słowo kluczowe percent, co oznacza procent szerokości (przy obcinaniu z lewej lub prawej strony) lub wysokości (przy obcinaniu z góry lub z dołu) obrazka. Czwarta część wyrażenia jest jednym ze słów kluczowych fromTop, fromBottom, fromLeft albo fromRight, które oznaczają z której strony obrazek ma zostać przycięty (odpowiednio: z góry, z dołu, z lewej albo z prawej strony).
formatowanie = "formatTo", format, usuwana strona
format = format predefiniowany | format dowolny
format predefiniowany = "Format10x15" | "Format15x10" | "Format21x15" | "Format15x21"
format dowolny = "format", "(", szerokość, ",", wysokość, ")"
usuwana strona = "removing", kierunki
kierunki = kierunki1 | kierunki2
kierunki1 = kierunek pionowy, "or", kierunek poziomy
kierunki2 = kierunek poziomy, "or", kierunek pionowy
Wyrażenie formatowanie służy do formatowania obrazka, czyli ustalania proporcji szerokości do wysokości. Składa się ze słowa kluczowego formatTo, po którym występuje nazwa uprzednio zdefiniowanego formatu albo wyrażenie składające się ze słowa format i dwóch wartości liczbowych oddzielonych przecinkiem i zawartych w nawiasach okrągłych. Stosunek tych wartości określa stosunek szerokości obrazka do jego wysokości. Dodatkowo należy umieścić w wyrażeniu wskazówki dotyczące tego, z której strony obrazek powinien zostać przycięty, jeśli to będzie wymagane. Obrazek może wymagać przycięcia z góry lub z dołu — w przypadku, gdy jest za wysoki (lub innymi słowy za wąski) w stosunku do żądanego formatu — albo z lewej lub z prawej strony w przypadku, gdy jest za niski (lub innymi słowy za szeroki).
zapis obrazka = "saveAs", plik
Wyrażenie zapis obrazka, służące do zapisania danych do pliku wynikowego, składa się z instrukcji saveAs, po której następuje nazwa pliku.
Plik Images.scala przedstawia implementację opisanego DSL.
Plik Images.scala: import javax.imageio.ImageIO import java.awt.image.BufferedImage import java.awt.Graphics2D import language.implicitConversions package images {
object LoadImage {
import java.io.File def fromDir(dir: String) = new LoadImage(dir)
def fromFile(filePath: String) = ImageIO.read(new File(filePath))
} class LoadImage(dir: String) {
import java.io.File def fromFile(filePath: String) = ImageIO.read(new File(dir, filePath)) } abstract sealed class Orientation
abstract sealed class VOrientation extends Orientation
abstract sealed class HOrientation extends Orientation
case object fromTop extends VOrientation case object fromBottom extends VOrientation case object fromLeft extends HOrientation case object fromRight extends HOrientation case class Format(w: Int, h: Int) {
def ratio:Double = w.toDouble/h.toDouble } object Format10x15 extends Format(10,15)
object Format15x10 extends Format(15,10) object Format21x15 extends Format(21,15) object Format15x21 extends Format(15,21) class FormatBuilder(bi: BufferedImage, format: Format) {
def removing(vo: VOrientation) = new {
def or(ho: HOrientation) = doFormat(bi,format,ho,vo) } def removing(ho: HOrientation) = new {
def or(vo: VOrientation) = doFormat(bi,format,ho,vo) } private def doFormat(bi: BufferedImage, format: Format,
ho: HOrientation, vo: VOrientation): BufferedImage = { val newWidth = (bi.getHeight.toDouble*format.ratio).toInt val newHeight = (bi.getWidth.toDouble/format.ratio).toInt if (bi.getWidth > newWidth) ho match { case `fromLeft` => bi.getSubimage(bi.getWidth-newWidth,0,newWidth,bi.getHeight) case `fromRight` => bi.getSubimage(0,0,newWidth,bi.getHeight) } else if (bi.getHeight > newHeight) vo match { case `fromTop` => bi.getSubimage(0,bi.getHeight-newHeight,bi.getWidth,newHeight) case `fromBottom` => bi.getSubimage(0,0,bi.getWidth,newHeight) } else bi } } class CropParam(bi: BufferedImage, value: Double) {
private def n = value.toInt private def h = (bi.getHeight*value/100).toInt private def w = (bi.getWidth*value/100).toInt def px(orientation: Orientation) = orientation match {
case `fromTop` => bi.getSubimage(0,n,bi.getWidth,bi.getHeight-n) case `fromBottom` => bi.getSubimage(0,0,bi.getWidth,bi.getHeight-n) case `fromLeft` => bi.getSubimage(n,0,bi.getWidth-n,bi.getHeight) case `fromRight` => bi.getSubimage(0,0,bi.getWidth-n,bi.getHeight) } def percent(orientation: Orientation) = orientation match {
case `fromTop` => bi.getSubimage(0,h,bi.getWidth,bi.getHeight-h) case `fromBottom` => bi.getSubimage(0,0,bi.getWidth,bi.getHeight-h) case `fromLeft` => bi.getSubimage(w,0,bi.getWidth-w,bi.getHeight) case `fromRight` => bi.getSubimage(0,0,bi.getWidth-w,bi.getHeight) } } } package object images {
implicit class ImageOps(bi: BufferedImage) {
def rotate(degrees: Double): BufferedImage = {
import java.awt._, image.AffineTransformOp, AffineTransformOp._, geom.AffineTransform._ val newBi = new BufferedImage(bi.getWidth, bi.getHeight, bi.getType) val angle = degrees*math.Pi/180.0 val transform = getRotateInstance(angle, bi.getWidth/2, bi.getHeight/2) new AffineTransformOp(transform, TYPE_BICUBIC). filter(bi, newBi) newBi } def formatTo(format: Format) = new FormatBuilder(bi, format)
def crop(n: Int) = new CropParam(bi, n)
def saveAs(filePath: String) {
import java.io.File ImageIO.write(bi, "jpg", new File(filePath)) } } }
Całość kodu należy do pakietu o nazwie images (wiersze
i
).
Pojedyncza klauzula import (taka jak w przykładowym skrypcie)
wystarczy, aby zaimportować wszystkie potrzebne elementy składowe
języka.
Wyrażenia języka rozpoczynają się od słowa LoadImage, które jest
nazwą obiektu. Jego definicja rozpoczyna się w wierszu
. Po słowie
LoadImage może wystąpić słowo fromDir lub fromFile. Obiekt
LoadImage zawiera definicje dwóch metod o takich właśnie nazwach
(wiersze
i
). Obie metody przyjmują po jednym argumencie typu
String. Użycie metody fromFile powoduje załadowanie danych z pliku
o podanej nazwie za pomocą metody ImageIO.read. Wynikiem działania
tej metody jest obiekt typu BufferedImage. W przypadku użycia metody
fromDir następuje utworzenie instancji klasy LoadImage, której
definicja rozpoczyna się w wierszu
. Klasa LoadImage zawiera
definicję jednej metody — o nazwie fromFile — której działanie jest
podobne do działania metody fromFile z obiektu LoadImage z tą
różnicą, że dodatkowo przy wczytywaniu pliku jest brana pod uwagę
podana nazwa katalogu. Użycie klasy LoadImage pozwala zapamiętać
podaną w argumencie metody fromDir ścieżkę katalogu i wykorzystać ją
dopiero w momencie wywołania metody fromFile. W pakiecie images są
zatem zdefiniowane dwie metody o nazwie fromFile, ale w obu
przypadkach ich wynikiem jest obiekt tego samego typu.
Bezpośrednio po wyrażeniu wczytującym dane z pliku składnia języka
pozwala zastosować kilka różnych instrukcji. Wszystkie one są
zaimplementowane w postaci metod zdefiniowanych w klasie ImageOps,
której implementacja znajduje się w obiekcie pakietu images (wiersz
i kolejne). Jednak metody fromFile nie zwracają obiektów klasy
ImageOps, ale obiekty typu BufferedImage. W związku z tym klasa
ImageOps ma parametr typu BufferedImage i jest zdefiniowana z
modyfikatorem implicit (wiersz
).
Metoda rotate (wiersz
) służy do obrócenia obrazka o określony kąt
wokół jego środka. Dane wymagane do wykonania takiej operacji są
dostępne z samego obiektu obrazka oraz przekazane jako parametr
metody. Wobec tego metoda rotate może od razu wykonać żądaną
operację i zwrócić nową instancję klasy BufferedImage. Inaczej jest
w przypadku metod formatTo (wiersz
) oraz crop (wiersz
).
Każda z nich rozpoczyna sekwencję wywołań metod, z których dopiero
ostatnia powoduje wykonanie właściwej operacji. Poprzedzające ją
metody z sekwencji służą do zapamiętywania danych na użytek tej —
mającej być dopiero wykonaną — operacji.
Metoda formatTo tworzy instancję klasy FormatBuilder (wiersz
),
przekazując jej w parametrach referencję do obrazka oraz docelowy
format. Wybór formatu obrazka, będącego argumentem metody formatTo,
może nastąpić za pomocą klasy przypadku Format (zdefiniowanej w
wierszu
) lub za pomocą jednego z obiektów dziedziczących z tej klasy
(wiersz
i kolejne). Po wywołaniu metody formatTo i następującym po
niej argumencie, następuje sekwencja wywołań metod removing oraz
or. Klasa FormatBuilder definiuje dwie różne metody removing,
gdyż argumentem tej metody może być albo obiekt typu VOrientation
(wiersz
), określający czy przyciąć obrazek z góry (fromTop), czy z
dołu (fromBottom), albo obiekt typu HOrientation (wiersz
),
określający czy przyciąć obrazek z lewej (fromLeft), czy z prawej
(fromRight) strony. Obie metody removing zwracają obiekty
anonimowe implementujące metodę o nazwie or. Metoda removing z
wiersza
, przyjmująca argument typu VOrientation, zwraca instancję,
której metoda or przyjmuje paramer typu HOrientation, natomiast
metoda removing z wiersza
, przyjmująca argument typu
HOrientation — odwrotnie — zwraca instancję, której metoda or
przyjmuje paramer typu VOrientation. Każda z metod or wywołuje
prywatną metodę doFormat (wiersz
), która dokonuje właściwej
operacji formatowania obrazka i zwraca instancję klasy
BufferedImage.
Metoda crop (wiersz
) tworzy instancję klasy CropParam (wiersz
),
przekazując jej w parametrach referencję do obrazka oraz liczbę,
określającą jak duży fragment ma zostać przycięty. Po wywołaniu
metody crop i następującym po niej argumencie, następuje wywołanie
metody px (wiersz
) lub percent (wiersz
). Obie metody
przyjmują jako parametr obiekt typu Orientation (wiersz
),
określający z której strony przyciąć obrazek. Metoda px interpretuje
argument metody crop jako liczbę pikseli, o jaką należy przyciąć
obrazek, natomiast metoda percent interpretuje go jako procent
szerokości lub wysokości obrazka. Obie metody przycinają obrazek i
zwracają instancję klasy BufferedImage.
Ostatnią z metod zdefiniowanych w klasie ImageOps jest metoda
saveAs (wiersz
), służąca do zapisania obrazka do pliku. Ta metoda
ma rezultat typu Unit i powinna być ostatnią z metod używanych w
sekwencji składającej się na wyrażenie opisywanego DSL. Oczywiście
wywoływanie kolejnych metod w sposób opisany wyżej w składni DSL jest
możliwe dzięki wykorzystaniu operacji infiksowych.
Plik CasaBattloScript.scala:
import images._ 