Bezstratna optymalizacja plików JPG

Czyta­jąc mądrą książkę doty­czącą opty­ma­li­za­cji witryn inter­ne­to­wych natra­fi­łem na narzę­dzie jpeg­tran, które wykona dla nas kawał dobrej roboty bezstrat­nie zmniej­sza­jąc wiel­kość plików jpg.1 Dodat­kowo przy okazji możemy wyrzu­cić część lub całość meta­da­nych z pliku.

Użyt­kow­nicy czasem sami sobie robią krzywdę wrzu­ca­jąc zdję­cia do inter­netu robione komór­kami z GPSem. Raz, że komórki podają swój model i inne para­me­try (np. przy­słony), a dwa, że zapi­sują w meta­da­nych dokładną loka­li­za­cję wyko­na­nego zdję­cia (a tym samym np. czyje­goś domu).

Zmniej­sze­nie wiel­ko­ści pliku za pomocą jpeg­tran polega na opty­ma­li­za­cji tablic Hauf­f­mana i ewen­tu­al­nie doda­nie trybu progre­syw­nego (w miarę łado­wa­nia się zdję­cia polep­sza się jego jakość na stro­nie WWW). Co ciekawe, doda­nie progre­syw­no­ści również zaosz­czę­dza kilka KB w zdjęciu.

Walka toczy się o ok. 20% wiel­ko­ści pliku, czyli dosyć sporo. Jpeg­tran można sobie w Ubuntu znaleźć w Synap­ticu w ramach pakietu libjpeg-progs. Co do Windowsa radź­cie sobie sami ;-)

Na koniec przed­sta­wię owoc mojej godzin­nej pracy: klasę, za pomocą której w php możemy stero­wać poczy­na­niami jpeg­tran. Jak zwykle po Angiel­sku, ale za to komen­ta­rze i errory po Polsku żeby nie było że jakiś stron­ni­czy jestem. Jak to cudo działa? Na etapie tworze­nia obiektu przyj­muje 2 parametry:

  1. Co zrobić z meta­da­nymi pliku
    • none — wyrzu­cić wszyst­kie metadane
    • comments — zosta­wić tylko komen­ta­rze (mogą zawie­rać dane na temat licen­cji i/lub właściciela)
    • all — zosta­wić meta­dane tak jak są
  2. Jakie wyko­nać opera­cje na pliku
    • opti­mise — prze­pro­wa­dza opty­ma­li­za­cję tablic Hauffmana
    • progres­sive — dodaje tryb progresywny

Wystar­czy sobie utwo­rzyć obiekt JpegOp­ti­mi­ser i można za pomocą metody proces­sI­mage prze­twa­rzać obrazki, np. tak:

$jpg = new JpgOptimiser('none', array('optimise','progressive'));
$jpg->processImage('test.jpg', 'test_wyjsciowy.jpg');

Poni­żej kod klasy:

<?php
class JpgOptimiser {
 
    /**
     * @var array tablica dopuszczalnych parametrow operacji graficznych
     */
    protected static $VALID_PROCS = array('optimise', 'progressive');
 
    /**
     * @var array tablica dopuszczalnych parametrow operacji na metadanych
     */
    protected static $VALID_META = array('none','all','comments');
 
    /**
     * @var array tablica najczestszych bledow z wyjscia i komunikat bledu
     */
    protected static $REGEX_OUTPUT_ERRORS = array(
            array('/jpegtran: not found/', 'nie wykryto jpegtran'),
            array('/can\'t open/', 'nie mozna otworzyc pliku'),
            array('/not a jpeg/i', 'to nie jest plik jpg')
    );
 
    /**
     * @var string przygotowana lista argumentow do uruchomienia w konsoli
     */
    protected $argsChain;
 
    /**
     * Wybor listy operacji do wykonania.
     * @param string $meta operacje wykonywane na metadanych; dopuszczalne: none, comments, all
     * @param array $procs operacje graficzne; dopuszczalne: optimise, progressive
     */
    public function __construct($meta = 'none', array $procs = array('optimise', 'progressive')) {
        $this->argsChain  = $this->parseArgs($procs, self::$VALID_PROCS, ' -');
        $this->argsChain .= $this->parseArgs(array($meta), self::$VALID_META, ' -copy ');
    }
 
    /**
     * @param array $args tablica parametrow wejsciowych
     * @param array $valid tablica parametrow dopuszczalnych
     * @param string $prefix znak poprzedzajacy argument
     * @return string przetworzony ciag argumentow
     */
    private function parseArgs(array $args, array $valid, $prefix) {
        $output = '';
        foreach ($args as $arg) {
            if (in_array($arg, $valid)) {
                // dorzucam myslnik przed parametrem i spacje na koncu
                $output .= $prefix . $arg . ' ';
            }
        }
        return $output;
    }
 
    /**
     * Przetwarzanie pliku.
     * Docelowo lepszym rozwiazaniem bedzie uzywanie SPLFileInfo.
     * @param string $srcFile nazwa pliku wejscowego
     * @param string $dstFile nazwa pliku wyjscowego (moze byc taka sama jak wejscowy)
     * @return bool
     */
    public function processImage($srcFile, $dstFile) {
        $output = shell_exec('jpegtran' . $this->argsChain . '-outfile ' . escapeshellarg($dstFile) . ' ' . escapeshellarg($srcFile) . ' 2>&1');
        try {
            return $this->isProcessingOK($output);
        }
        catch(Exception $e) {
            echo $e->getMessage();
            return false;
        }
    }
 
    /**
     * @param mixed $output
     * @return bool
     * @throws Exception
     */
    private function isProcessingOK($output) {
        if ($output === null) {
            return true;
        }
        else {
            // szukam znanych komunikatow bledow
            foreach (self::$REGEX_OUTPUT_ERRORS as $error) {
                if (preg_match($error[0], $output) != 0) {
                    throw new Exception($error[1]);
                }
            }
            // nie znaleziono znanych, rzucam cala tresc wyjscia
            throw new Exception($output);
        }
    }
}
  1. S. Souders : Jesz­cze wydaj­niej­sze witryny inter­ne­towe. Przy­spie­sza­nie dzia­ła­nia serwi­sów WWW. Gliwice : Helion, 2010, s. 156–157.

Podobne wpisy:

  1. Zapi­sy­wa­nie plików flash oglą­da­nych w przeglądarce
  2. Tworze­nie, dzie­le­nie i łącze­nie plików PDF w Ubuntu
  3. Minia­turki plików wideo w GNOME
  4. Wygrze­bane z GitHuba (1) : CSS Crush

5 Comments

  • Anonim
    11 października 2010 - 11:51 | Permalink

    Opty­ma­li­za­cja tablic zazwy­czaj nie daje wiele, bo sam algo­rytm Huff­mana jest dosyć opty­malny, a w JPG to jedyne możliwe do opty­ma­li­za­cji miej­sce bez inge­ren­cji w dane i specy­fi­ka­cję formatu, tak więc faktycz­nie bezstratne jak piszesz w tema­cie. Usuwa­nie exif, komen­ta­rzy i innych meta­da­nych to już naru­sze­nie i jest utratą danych, co kłóci się z tytu­łem wpisu :) Oczy­wi­ście można znacząco zredu­ko­wać wiel­kość pliku usuwa­jąc je. Im mniej­szy plik wejściowy tym więk­szy procent „zysku”.

    Co do trybu progre­syw­nego JPG to nie wszyst­kie prze­glą­darki go obsłu­gują mimo tylu lat obec­no­ści. Podob­nie jak sprawa się ma z prze­zro­czy­sto­ścią w PNG.

    Ogól­nie rzecz biorąc to bez doty­ka­nia meta­da­nych z JPG nie wyci­śniemy wiele w trak­cie opty­ma­li­za­cji. Jeste­śmy bowiem zmuszeni do korzy­sta­nia z okre­ślo­nego algo­rytmu i koniec. Możemy jedy­nie kombi­no­wać z ciągiem wejścio­wym do niego lub opie­rać się na drob­nych mody­fi­ka­cjach algo­rytmu, a na to ostat­nie specy­fi­ka­cja nie pozwala. Pozo­staje nam więc jedy­nie opty­ma­li­za­cja ciągu wejściowego

  • thek
    11 października 2010 - 12:00 | Permalink

    Zapo­mniał­bym… Wpis powy­żej jest mojego autor­stwa. Drzewa Huff­mana sam pisałem/wyliczałem, bo tech­niki multi­me­dialne to była część mojej specja­li­za­cji na studiach. Tak więc metody kompre­sji obra­zów czy dźwięku, podsta­wowe, prze­ra­bia­łem. Huff­man też był w nich ujęty. Ogól­nie ciężko w tym algo­ryt­mie cokol­wiek optymalizować.

  • Śpiechu
    11 października 2010 - 14:38 | Permalink

    @thek
    Dzięki za komen­ta­rze. Z prze­pro­wa­dzo­nych testów wynika, że w zależ­no­ści od programu gene­ru­ją­cego jpg różnie to bywa z opty­ma­li­za­cją. W najgor­szym wypadku nie zyskamy nic (a raczej stra­cimy trochę mocy prze­ro­bo­wych serwera).
    Co do meta­da­nych, to tak jak mówię: nie zawsze użyt­kow­nik ma świa­do­mość ile danych o sobie udostęp­nia wrzu­ca­jąc gdzieś swoje zdję­cia.
    Meta­dane można oczy­wi­ście wyrzu­cać selek­tyw­nie (widzia­łem gdzieś nawet pakiet klas w php do mani­pu­la­cji w EXIF).

  • thek
    11 października 2010 - 20:34 | Permalink

    Może dodam jak najczę­ściej się opty­ma­li­zuje te drzewa kodów na 2 sposoby. Niestety uwierz, ale nie są one bezstratne albo do końca zgodne ze specyfikacją.

    Pierw­szy to okre­śle­nie praw­do­po­do­bień­stwa, poni­żej którego wszyst­kie „pod kreską” są zastę­po­wane jednym kolo­rem. Najczę­ściej jest ono tak niskie, że ludz­kie oko nie dostrzega tego, bo kolory obrazu są w tak zniko­mej ilości.

    Drugi to okre­śle­nie długo­ści słowa, powy­żej której sens traci dalsze kodo­wa­nie i nie stosuje się go, tylko prze­pusz­cza dane nieza­ko­do­wane. Najczę­ściej długość słowa w kodzie jest wtedy dłuż­sza niż nieko­do­wana i lepiej wręcz nie kodo­wać tych najrza­dziej występujących.

  • 5 grudnia 2010 - 12:21 | Permalink

    A mi się bardzo podoba, przy­datna zabawka. Tak jak wyżej zostało napi­sane, często udostęp­niamy mnóstwo meta­da­nych o nas, co najczę­ściej jest niepo­trzeb­nym zabie­giem (z powodu prywat­no­ści i oszczęd­no­ści trans­feru). Bardzo fajna klasa!

  • Dodaj komentarz

    Twój adres e-mail nie zostanie opublikowany.

    Możesz użyć następujących tagów oraz atrybutów HTML-a: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <p> <pre lang="" line="" escaped=""> <q cite=""> <strike> <strong>