Wsadowe przetwarzanie obrazków

Namalowanie czy skorygowanie kilku obrazków w programie graficznym może być przyjemnością. Ale analogiczne obsługiwanie kilkuset to udręka. Zwłaszcza, gdy do każdego trzeba dodać taką samą rameczkę czy wprowadzić tę samą korektę tła. Albo, co gorsza, od czasu do czasu te atrybuty zmieniać.

Dużo lepszym rozwiązaniem może być niewielki skrypt.

Dużo poręczniej może być napisać krótki skrypt, który wykona pożądaną operację, dając też możliwość jej powtórzenia w razie potrzeby.

ImageMagick

ImageMagick (oraz jego fork GraphicsMagick) to obszerne biblioteki funkcji graficznych, dostępnych dla programów pisanych w C/C++, javie, perlu, pythonie i wielu innych językach (patrz np. GraphicsMagick Programming Interfaces). Funkcje są też dostępne dla skryptów shellowych (programy convert i animate).

Różnice między ImageMagick i GraphicsMagick są niewielkie i dotyczą zaawansoawnych zastosowań. Dla początkującego użytkownika istotniejsza będzie wygoda instalacji - pod Linuxami lepiej spaczkowany jest zwykle ImageMagick, za to pod Windows wygodniej instaluje się GraphicsMagick.

Biblioteki są bardzo obszerne, działają też naprawdę sprawnie. Niestety ... nie ma róży bez kolców. Dokumentacja jest fatalna. Odgadnięcie jaką funkcję i z jakimi parametrami należy wywołać przypomina grę w zgadywanki (albo desperackie przeszukiwanie internetu w nadziei na trafienie na zbliżony przykład).

Poniżej prosty przykład w perlu (używa GraphicsMagick ale przejście na ImageMagick to po prostu zastąpienie wszystkich napisów Graphics::Magick przez Image::Magick). Skrypt znajduje wszystkie obrazki w katalogu uruchomienia i jego podkatalogach i tworzy ich kopie z przebitką złotego koloru (jakaś odmiana tej techniki może służyć do robienia podświetlonych wersji obrazków):

#!/usr/bin/perl
# -*- coding: utf-8 -*-
use strict;

use File::Find;
use Graphics::Magick;

sub patch_image {
    my ($image_file_name, $output_file_name) = @_;

    my $img = new Graphics::Magick;

    my $x = $img->Read($image_file_name);
    die "Read($image_file_name) failed: $x\n" if ($x);

    # W razie wątpliwości można umieszczać w kodzie dyrektywy
    # jak poniższa, by podejrzeć obrazek w różnych fazach jego
    # przetwarzania. Każde wywołanie Display powoduje uruchomienie
    # się podglądu obrazka - i zawieszenie dalszego działania skryptu
    # do czasu jego zamknięcia
    #
    # $img->Display();

    $x = $img->Colorize(fill => 'yellow', opacity => 40);
    die "Colorize failed: $x\n" if ($x);

    $x = $img->Write($output_file_name);
    die "Write($output_file_name) failed: $x\n" if ($x);
}

# Szukamy w katalogu uruchomienia skryptu i jego podkatalogach (.)
# wszystkich plików png, jpg, jpeg i gif, dla każdego robimy kopię.
find (sub {
          next unless -f;
          if (/^(.*)\.(png|jp?g|gif)$/) {
              patch_image($_, $1 . "_gold." . $2);
          }
      },
      '.');

W następujący sposób tworzymy prostokąt o ustalonym rozmiarze wypełniony jednolitym kolorem:

my $img = Graphics::Magick->new(size => $width . 'x' . $height);
$img->ReadImage('xc:#AA0000');

Niezależną kopię obrazka tworzymy wołając:

my $imgcopy = $img->Clone();

Itd itp.

Ważna uwaga: obiekt tworzony przez Graphics::Magick->new nie jest tak naprawdę jednym obrazkiem, ale listą obrazków (zbliżoną do listy warstw w programie graficznym). Zależnie od tego jak zostaną obsłużone, mogą zostać potraktowane faktycznie jako warstwy, bądź np. jako kolejne klatki animacji. A warstwy z dwóch obrazków łączymy łącząc tablice, np:

push @$main_img, @$other_img;

Jak już wspomniałem, zamiast perla można użyć wielu innych języków programowania, a nawet pisać skrypty shella (wykorzystujące programy convert, composite, montage, identify, mogrify itp).

Więcej przykładów nie będzie, bo po spędzeniu kiedyś dwóch godzin na kombinowaniu co wywołać i z jakimi parametrami by ułożyć kilka obrazków w tabelkę stwierdziłem, że siła i wydajność Graphics::Magick nie jest warta mojego wysiłku. Może ktoś kiedyś udokumentuje tę bibliotekę... No, na koniec ten przykład:

my $img = new Graphics::Magick;
push @$img, $lefttop->Clone();
push @$img, $righttop->Clone();
push @$img, $leftdown->Clone();
push @$img, $rightdown->Clone();
$img = $img->Montage(
      background=>'pink',
      borderwidth => 0,
      geometry => '2x2+0+0',
      gravity => 'South',
      shadow => 0,
      tile => '2x2',
     );

GIMP jako biblioteka

GIMP jest powszechnie znanym, silnym programem graficznym. Znacznie mniej znany jest fakt, że funkcjonalność GIMPa można wykorzystywać także przy pomocy skryptów w perlu i pythonie.

Przykład prostego skryptu w perlu (tworzymy obrazek z lekką płucienną teksturą). Oczywiście w praktyce będziemy jednym skryptem opracowywać wiele obrazków, ale pozostawiłem tylko prosty przykład:

#!/usr/bin/perl
# -*- coding: utf-8 -*-
use strict;

# Jeśli używamy opcji uruchomieniowych, należy je
# przetworzyć przed załadowaniem poniższych modułów
# i wyczyścić zmienną ARGV, inaczej moduł Gimp
# może próbować sam je przetwarzać.

use Gimp qw(:auto);
use Gimp::Fu;

# Odkomentowanie poniższego spowoduje szczegółowe
# wypisywanie wszystkich wywoływanych funkcji
# i ich parametrów
# Gimp::set_trace(TRACE_ALL);

# Inicjalizacja engine gimpa. Można go następnie używać
# wielokrotnie.
Gimp::init;

my $size = 64;
my $base_color = '#AA0000';
my $output_name = "output.png";

# Tworzymy nowy pusty obrazek
my $img = Gimp->image_new($size, $size, RGB);

# Tworzymy warstwę i dodajemy ją do obrazka
my $layer = $img->layer_new($size, $size, RGB_IMAGE,
                            "Bgr", 1.0, NORMAL_MODE);
$img->add_layer($layer, 0);

# Ustalamy kolor tła i wypełniamy nim powyższą warstwę
Gimp->context_set_background($base_color);
$layer->drawable_fill(BACKGROUND_FILL);

# Nakładamy wzorzec płótna
$layer->plug_in_apply_canvas(0, 1);

# Zapisujemy wynik
$layer->file_save($output_name, $output_name);

Ogromną zaletą jest tutaj zbieżność wykorzystywanego API z funkcjami używanymi przy pomocy interfejsu GIMPa. Standardową metodą pisania skryptów jest przećwiczenie sekwencji operacji w GIMPie, a następnie zapisanie jej jako skryptu. Zalecane jest używanie anglojęzycznego interfejsu, wówczas wołane procedury nazywają się podobnie do opcji menu.

Nazwy funkcji i ich opis odnajdujemy przy pomocy wbudowanej w GIMPa przeglądarki procedur (Dodatki/Przeglądarka procedur czy też Xtns/Procedure Browser). Wpisujemy fragment nazwy (najprościej go odgadywać pracując z GIMPem anglojęzycznym, wówczas nazwy funkcji są w większości bardzo zbliżone do nazw pozycji w menu), dostajemy listę funkcji i ich (zwykle szczegółowe i dokładne) opisy. Jeśli funkcja odpowiada jakiejś opcji obsługiwanej oknem dialogowym (np. gdy wołamy funkcje plugin_, parametry niemal zawsze odpowiadają ustawieniom dostępnym w tym okienku).

Prefiks gimp_ można niemal zawsze pominąć, jeśli (jak wyzej) posługujemy się składnią obiektowego wywołania na obiekcie warstwy, to nie ma potrzeby przekazywania obiektu obrazka.

Parę innych (fragmentarycznych) przykładów.

Przebalansowanie kolorów:

my $min_level = 50;
my $max_level = 255;
$layer->gimp_levels(HISTOGRAM_VALUE, 0, 255, 1.0,
                    $min_level, $max_level);

Sprawdzenie czy plik PNG jest indeksowany i jeśli nie, skonwertowanie go na taką wersję (zbliżony efekt dałby plugin_webify):

my $source_file = 'input.png';
my $destination_file = 'output.png';
my $color_count = 16;

my $img = Gimp->file_load($source_file, $source_file);

my $drawable = $img->get_active_drawable;
unless ($drawable->is_indexed()) {
    $img->gimp_image_convert_indexed(
        2, # FSLOWBLEED_DITHER,
        0, # MAKE_PALETTE,
        $color_count,
        0, # dithering alpha
        0, # remove_unused
        '' # custom palette
       );
}
else {
   print "$source_file already indexed\n";
}

$img->get_active_drawable->file_save(
    $destination_file, $destination_file);

(powyższy skrypt jest ogólnie użyteczny, wystarczy go uzupełnić o wyszukiwanie plików w drzewie katalogów i może służyć do finanej obróbki obrazków przed publikacją).

Nałożenie obrazka na drugi obrazek (np. nakładanie grafiki na tło):

unless (($background->image_width eq $graphics->image_width)
     && ($background->image_height eq $graphics->image_height)) {
    die "Size mismatch\n";
}

my $img = $background->duplicate();
$graphics->selection_none();
$graphics->get_active_drawable->edit_copy() or die "Nothing to copy";
$img->get_active_drawable->edit_paste(0);
$img->selection_none();
$img->merge_visible_layers(CLIP_TO_IMAGE);

Podkoloryzowanie (przez nałożenie półprzezroczystej warstwy zadanego koloru):

my $layer = $image->layer_new(
    $image->image_width, $image->image_height, RGBA_IMAGE,
    "layercolor", $opacity, NORMAL_MODE);
$image->add_layer($layer, -1);

Gimp->context_set_background($color);
$layer->drawable_fill(BACKGROUND_FILL);

$image->merge_visible_layers(CLIP_TO_IMAGE);

Namalowanie ramki dookoła obrazka:

my $depth = 2;
my $width = $image->image_width;
my $height = $image->image_height;
$image->rect_select($depth, $depth,
     $width - 2 * $depth, $height - 2 * $depth,
     CHANNEL_OP_REPLACE, 0, 0);
$image->selection_invert();

Gimp->context_set_background($color);
$image->get_active_drawable->edit_fill(BACKGROUND_FILL);

$image->selection_none();

Skrypty korzystające z GIMPa w opisany wyżej sposób tak naprawdę uruchamiają jego engine (bez interfejsu). Wydajność nie jest najlepsza (znacznie wolniej, niż przy pomocy ImageMagick) ale rzadko jest to istatony problem. A wygoda pisania to rekompensuje.

Oprócz perla, można także używać Pythona (oraz, kto lubi, Script-Fu). Wykorzystując zaś to samo API piszemy również pluginy do GIMPa.

SVG

Jeśli różnorodność obrazków sprowadza się do różnych rozmiarów (np. jakieś przyciski w kilku rozmiarach), interesującą alternatywą może być namalowanie wektorowej wersji obrazka.

Bitmapy do używania na webie generujemy np. przy pomocy Inkscape. Program ten może być używany wsadowo do generowania bitmap zadanej wielkości z podanego obrazka SVG lub jego części. Np. tak:

inkscape --export-png=output.png \
   --export-width=60 --export-height=60 \
   source.svg

lub tak (gdy chcemy tylko fragment):

inkscape --export-png=output.png \
   --export-width=60 --export-height=60 \
   --export-area='23.055:11.0:119.055:107.0' \
   source.svg

Podanie obszaru jest przydatne zwłaszcza, gdy tworzymy jeden spory plik SVG malując na nim różne obrazki. Obszar zadajemy współrzędnymi lewego dolnego a następnie prawego górnego rogu. Powyższe uruchomienie bywa czułe na opcje językowe, w razie kłopotów z interpretacja liczb dodajemy LANG=C.

Jedyny (acz nieprzyjemny) problem: gdy coś idzie źle, najczęściej program inkscape po prostu po cichu nie tworzy wyniku.

Pliki SVG są też dosyć podatne na edycję - są to po prostu pliki XML, niektóre transformacje można wykonać realizując proste przetwarzanie XML. Są też dostępne bardziej zaawansowane biblioteki, jak Cairo.

Skrypty PhotoShop

Adobe PhotoShop posiada wbudowaną funkcjonalność nagrywania i uruchamiania złożonych makr - skryptów. Niektóre inne programy graficzne - podobnie.

Technik tych nie opisuję, bo sam nie testowałem, są jednak niekiedy warte rozważenia. Tym bardziej, że w wielu wypadkach treść skryptu można nagrać po prostu ją realizując.

komentarze obsługiwane przez Disqus