Wydobywanie tekstów i grafik z plików PDF

Bywa że klient przysyła Ci treści do umieszczenia na stronie z krótkim komentarzem – „Tak to sobie wyobrażam”. Otwierasz maila i widzisz plik PDF. Są tam pięknie ułożone teksty okraszone kolorowymi fotografiami, wszystko elegancko poukładane i skomponowane, a Ty drapiesz się w głowę i nic nie mówisz bo szkoda słów. Jeśli jesteś webdeveloperem, specjalistą od cięcia i stylowania to szczerze Ci współczuję. Jeśli jednak Twoim jedynym zadaniem jest wydobycie z tego PDF-a wszystkich tekstów oraz grafik to jest nadzieja.

Klikacze lub marzyciele często patrzący w „okna” mogą skorzystać z jednej z rad proponowanych w artykule How to Extract Text from a PDF document lub How Can I Get Text or Images Out of a PDF File?. Alternatywą jest użycie konsoli i wierzcie mi w tym przypadku jest to o wiele wygodniejsze i szybsze rozwiązanie.

Do wyłuskania tekstów używamy programu „pdftotext” będącego częścią programu „xpdf

pdftotext ./dokument.pdf

Gdyby były problemy z polskimi literami warto zapoznać się z opcją „enc”:

pdftotext -enc Latin2 dokument.pdf

albo

pdftotext -enc UTF-8 dokument.pdf

Z obrazkami robimy identycznie tylko, że używając programu „pdfimages” także będącego konsolowym narzędziem „xpdf-a”

pdfimages ./dokument.pdf  przedrostek_obrazka

Program „pdfimages” zapisuje obrazki w formacie plików „ppm” więc trzeba je jeszcze przekonwertować do jpg-ów np. przy pomocy programu „pnmtojpeg” będącego częścią większego pakietu programów graficznych „Netpbm„.

for pic in *.ppm 
do 
    pnmtojpeg "${pic}" > "${pic/%ppm/jpg}" 
done

O ile programy „pdftotext” jak i „pdfimages” miałem już w systemie to „pnmtojpeg” trzeba było doinstalować i kiedy już to zrobiłem i przetestowałem przypomniałem sobie, że mam przecież zainstalowanego „ImageMagick-a„, którego użycie okazało się jeszcze prostsze.

mogrify -format jpg *.ppm

Pliki wynikowe uzyskane po zastosowaniu „mogrify” są większe co sugeruje mniejszą stratę na jakości, poza tym jak się ma „ImageMagick-a” to można z tymi plikami zrobić przy okazji dużo więcej np. automatycznie przeskalować, utworzyć miniatury, dodać ramki czy co nam tam jeszcze przyjdzie do głowy. Tworząc skrypt, który wszystkie wyżej przytoczone komendy zbiera w jedną zdecydowałem się właśnie na „mogrify”.

Plik extractpdf.sh

#!/bin/sh
 
if [ $# -lt 1 ]; then
    echo "Usage: `basename $0` file.pdf"
    exit 1;
fi
 
PDF=$1
FILE_NAME=`basename ${PDF%.*}`
TEMP_DIR="`dirname $PDF`/${FILE_NAME}"
 
if [ ! -e $TEMP_DIR ]; then
    mkdir -p $TEMP_DIR;
fi
pdftotext $PDF "${TEMP_DIR}/${FILE_NAME}.txt"
 
pdfimages $PDF "${TEMP_DIR}/${FILE_NAME}"
 
mogrify -format jpg ${TEMP_DIR}/*.ppm
find ${TEMP_DIR}/ -name "*.ppm" -exec rm {} \;

Skrypt oszczędza masę pisania poza tym jest uniwersalny więc nadaje się do wielokrotnego użytku. Teraz wystarczy nadać mu prawa do wykonywania

chmod +x ./extractpdf.sh

i cała praca to wywołanie skryptu z podaniem ścieżki do pliku pdf w parametrze

./extractpdf.sh ./dokument.pdf

Można pokusić się o rozbudowę tego skryptu o sprawdzanie czy wymagane programy są zainstalowane oraz zwiększyć funkcjonalność poprzez próbę użycia „pnmtojpeg” w przypadku jeśli „ImageMagick” nie jest zainstalowany.

Plik extractpdf2.sh

#!/bin/bash
 
if [ $# -lt 1 ]; then
    echo "Usage: `basename $0` file.pdf"
	exit 1;
fi
 
type -P pdftotext &>/dev/null || { echo "I require pdftotext but it's not installed.  Aborting." >&2; exit 1; }
 
type -P pdfimages &>/dev/null || { echo "I require pdfimages but it's not installed.  Aborting." >&2; exit 1; }
 
if type -P mogrify >/dev/null; then
    CONVERTER="mogrify"
else
    type -P pnmtojpeg &>/dev/null || { echo "I require the mogrify or pnmtojpeg but none of them is not installed.  Aborting." >&2; exit 1; } 
    CONVERTER="pnmtojpeg"
fi
 
PDF=$1
FILE_NAME=`basename ${PDF%.*}`
TEMP_DIR="`dirname $PDF`/${FILE_NAME}"
 
if [ ! -e $TEMP_DIR ]; then
    mkdir -p $TEMP_DIR;
fi
 
pdftotext $PDF "${TEMP_DIR}/${FILE_NAME}.txt"
 
pdfimages $PDF "${TEMP_DIR}/${FILE_NAME}"
 
if [ $CONVERTER = 'mogrify' ]; then
    mogrify -format jpg ${TEMP_DIR}/*.ppm
    find ${TEMP_DIR}/ -name "*.ppm" -exec rm {} \;
else
    for PIC in ${TEMP_DIR}/*.ppm
    do
        pnmtojpeg "${PIC}" > "${PIC/%ppm/jpg}"
        rm $PIC
    done
fi

UWAGA!!! Użycie „type -P” wymaga skorzystania konkretnie z powłoki „bash” gdyż w „sh” program „type” nie ma opcji „-P” i traktuje ją jak ścieżkę do pliku co kończy się komunikatem błędu (-P: not found). Dlatego pomimo, że plik dalej nazywa się extractpdf.sh to zamiast „#!/bin/sh” należy wpisać „#!/bin/bash”.

To oczywiście nie wyczerpuje naszych możliwości. Zaproponowane przeze mnie narzędzie to niezbędne minimum. Stworzenie tego skryptu kosztowało mnie trochę czasu ale jest to praca jednorazowa. Podobnie miałem ze skryptem do generowania miniatur za to obecnie stale mam go w swoim arsenale i wierzcie mi, że suma sumarum zaoszczędził mi już naprawdę wiele czasu.

Posted in Bash by Zbigniew Heintze · Tag:

Domknięcia, funkcje anonimowe przestrzenie nazw i dekoratory

Pojęcie dekoratora w języku Python to coś więcej niż wzorzec projektowy. To elegancki sposób na zwiększenie możliwości danej funkcji czy metody doskonale wykorzystujący unikalne cechy języka Python. Mechanizm działania dekoratorów oraz sposób ich użycia doskonale opisał Kent S Johnson (opracowanie to można znaleźć także w języku polskim).

Prostym i użytecznym przykładem dekoratora może być np. funkcja mierząca czas wykonania funkcji udekorowanej.

import time
 
def timeit(method):
    """Mierzy czas wykonania funkcji"""
 
    def check_time(*args, **kw):
        ts = time.time()
        result = method(*args, **kw)
        te = time.time()
        log(method.__name__, args, kw, ts, te)
        return result
 
    def log(name, args, kw, time_start, time_end):
        msg = '%r (%r, %r) %2.10f sec' % (name, args, kw, time_end - time_start)
        print msg
 
    check_time.__name__ = method.__name__
    return check_time

Użycie dekoratora w Pythonie to syntaktyczna perełka.

@timeit
def say_hello(param):
    print param
    time.sleep(0.5)

W PHP osiągnięcie czegoś takiego jest po prostu niemożliwe. Od wersji PHP 5.3 język został wzbogacony o nowe cechy takie jak domknięcia, funkcje anonimowe i przestrzenie nazw. Czy owe mechanizmy umożliwiają implementację czegoś co choćby przypominało pythonowy dekorator?

W PHP nie da się zdefiniować funkcji, a potem przypisać jej inną nazwę.

def foo():
    print 'OK'
 
faa = foo
 
faa() # w wyniku otrzymamy: OK

no chyba że użyjemy funkcji anonimowej (ang. anonymous function)

$foo = function() {echo 'OK';};
$faa = $foo;
$faa(); // w wyniku otrzymamy: OK

Dekorowanie funkcji anonimowych przy użyciu domknięć

Możemy zapomnieć (na razie) o dekorowaniu tradycyjnych funkcji ale udekorowanie funkcji anonimowej staje się potencjalnie możliwe.

$foo = function() {echo 'OK';};
 
$foo = function() use ($foo) {
    echo 'Jest '; return $foo();
};
 
$foo(); // w wyniku otrzymamy: Jest OK

Domknięcia (ang. closures) stoją u podstaw programowania w Pythonie, są też od zawsze obecne i powszechnie używane np. w Javascript-cie.

var Example = function()
{ 
    this.public = function() 
    { 
        return "This is a public method"; 
    }; 
 
    var private = function() 
    { 
        return "This is a private method"; 
    };
};
 
Example.public()  // returns "This is a public method" 
Example.private() // error - doesn't work

Ich implementacja w PHP to kwestia dyskusyjna podobnie jak ich użyteczność. Z uwagi na określone cechy PHP domknięcia w tym języku wydają się nie być odpowiednikiem tego mechanizmu znanym z innych języków programowania. W przeprowadzanym przeze mnie teście przydały się jednak i osobiście dostrzegam jeszcze kilka innych ich praktycznych zastosowań. Wracając jednak do głównego wątku tego wpisu.

W miarę prosty sposób udało się udekorować funkcję „foo”. Domknięcie można zamknąć w funkcji dzięki czemu nadaje się do wielokrotnego użytku.

function timeit($func) {
    return function() use ($func) {
        // pobranie wszystkich argumentów funkcji
        $args = func_get_args();
 
        // utworzenie funkcji pomocniczej
        $microtime_float = function()
        {
            list($usec, $sec) = explode(" ", microtime());
            return ((float)$usec + (float)$sec);
        };
 
        // rozpoczęcie pomiaru czasu
        $time_start = $microtime_float();
 
        // wywolanie funkcji
        $result = call_user_func_array($func, $args);
 
        // zakończenie pomiaru czasu i wyświetlenie wyników
        $time_end = $microtime_float();
        $time = $time_end - $time_start;
        printf ("\nfunction was executed during %01.6f seconds\n", $time);
 
        // zwrócenie resultatu działania funkcji
        return $result;
    };
}

Zaprezentuje może użycie „dekoratora” timeit na funkcji wymagającej parametrów.

$func_say_hello = function(param) {
	return 'Hello '. $param;
};
 
$func_say_hello = timeit($func_say_hello);
echo $func_say_hello('World');

Funkcje anonimowe są, a raczej będą stosunkowo rzadko używane, podobnie zresztą jak funkcje lambda w Pythonie, których są odpowiednikiem. Pomiar czasu wykonania np. funkcji sortującej to z pewnością przydatna sprawa, ale wzbogacanie funkcji anonimowych o nowe możliwości przy pomocy „dekoratorów” wydaje się być mało przydatne. Czy zatem można udekorować zwykłą funkcję?

Czy przestrzenie nazw pomagają w dekorowaniu funkcji?

Po co nam przestrzenie nazw (ang. namespaces)? Załóżmy, że mamy w pliku functions.php zdefiniowaną przestrzeń nazw „lib” i funkcję „say_hello”

<?php
namespace lib {
    function say_hello($param)
    {
        return 'Hello '. $param;
    }
}

Gdybyśmy w pliku index.php dołączyli plik functions.php moglibyśmy używać zdefiniowanych w nim funkcji.

<?php
include './functions.php';
echo \lib\say_hello('World');

Wywołanie say_hello(‚World’); bez podania poprzedzającego go namespace-a zakończyłoby się błędem. Niestety PHP nie wspiera jak na razie możliwości aliasowania funkcji czy stałych zamkniętych w przestrzeni nazw dlatego też chęć odwoływania się do funkcji „say_hello” bezpośrednio musi zostać poprzedzona definicją nazwijmy to „atrapy”, która przy okazji pozwoli udekorować opakowywowaną funkcję.

include './functions.php';
 
function timeit($func) {...}
 
function say_hello($param)
{
    $func_say_hello = '\lib\say_hello';
    $func_say_hello = timeit($func_say_hello);
    $result = $func_say_hello($param);
}
echo say_hello('World');

Zwróćmy uwagę na to, że funkcje „say_hello” i „\lib\say_hello” to nie to samo i równie dobrze moglibyśmy nie używać przestrzeni nazw i osiągnęlibyśmy to samo (tylko nazwy funkcji byłyby inne)

function timeit($func) {...}
 
function say_hello($param) {...}
 
function decorated_say_hello($param)
{
    $func_say_hello = 'say_hello';
    $func_say_hello = timeit($func_say_hello);
    $result = $func_say_hello($param);
}

Z punktu widzenia wygody użycia to czy użyjemy przestrzeni nazw czy nie to mamy to samo. I tak chcąc zmierzyć czas wykonania funkcji „say_hello” musimy zmienić jej wywołanie w kodzie na „decorated_say_hello”. Nie da się udekorować zdefiniowanych funkcji w pythoniczny sposób.

Aliasy – nowe możliwości dekorowania klas

W przypadku funkcji moje rozważania to jedynie gimnastyka mózgu, jednak w przypadku klas teoretyczne dywagacje – jak się zaraz okaże – przybiorą praktyczną formę.

Wspominałem wyżej o aliasach. Tego czego nie da się zrobić z funkcjami i stałymi w PHP > 5.3 da się uczynić z klasami.

Załóżmy, że plik functions.php wygląda tak

namespace lib {
	class Say
	{
		public static function hello($param)
		{
			return 'Hello '. $param;
		}
 
		public function goodbye($param)
		{
			return 'Goodbye '.$param;
		}
	}
}

Do pliku index.php możemy dołączyć functions.php i przypisać klasie „/lib/Say” alias Say

include './functions.php';
 
function timeit($func) {...}
 
use \lib\Say as Say;
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Nadanie aliasu sprawia, że możemy używać klasy tak jakby była zdefiniowana bez użycia przestrzeni nazw. Jeśli w tym momencie przyjdzie nam ochota zmierzenia czasu wykonania metod klasy „Say” (a właściwie „\lib\Say”) bez zmiany wywołań musimy zrobić dwie rzeczy. Zakomentować linijkę nadającą alias oraz udekorować klasę.

include './functions.php';
 
function timeit($func) {...}
 
//use \lib\Say as Say;
 
class Say
{
	private $_obj;
 
	public function  __construct() {
		$this->_obj = new \lib\Say();
	}
 
	public static function hello($param)
	{
		$func = '\lib\Say::hello';
		$func = \deco\timeit($func);
		return $func($param);
	}
 
	public function goodbye($param)
	{
		$func = function($obj, $param) {
			return $obj->goodbye($param);
		};
 
		$func = \deco\timeit($func);
		return $func($this->_obj, $param);
	}
}
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Wywołanie metod się nie zmieniło, ale wzbogaciliśmy naszą klasę o funkcjonalność pozwalającą nam zmierzyć czas wykonania zarówno metody statycznej klasy, jak i metody instancji. Do wygody użycia dekoratora „@timeit” w Pythonie jest jeszcze bardzo daleko. Udekorowaliśmy zaledwie dwie metody, a trzeba było napisać tyle kodu. Z każdą kolejną metodą przybędzie dodatkowego kodu w dekoratorze. Czy da się ten przyrost ograniczyć?. Dzięki metodom magicznym da się zrobić bardziej uniwersalny dekorator.

include './functions.php';
 
function timeit($func) {...}
 
//use \lib\Say as Say;
 
class Say
{
	private $_obj;
 
	public function  __construct() {
		$this->_obj = new \lib\Say();
	}
 
	public static function  __callStatic($name, $arguments)
	{
		$func = '\lib\Say::'.$name;
		return self::_addDecorators($func, $arguments);
	}
 
	public function __call($name, $arguments)
	{
		$func = function() {
			$params = func_get_args();
			$obj = array_shift($params);
			$method = array_shift($params);
			return call_user_func_array(array($obj, $method), $params);
		};
 
		array_unshift($arguments, $name);
		array_unshift($arguments, $this->_obj);
		return self::_addDecorators($func, $arguments);
	}
 
	private static function _addDecorators($func, $arguments)
	{
		$func = \deco\timeit($func);
		return call_user_func_array($func, $arguments);
	}
}
 
echo Say::hello('Adam');
$say = new Say();
echo $say->goodbye('Ewa');

Konkluzje

1. To naprawdę działa – doznałem intelektualnego orgazmu.
2. Nie stosujcie takich rozwiązań w prawdziwym projekcie

Próba debugowania takiego kodu skazana jest z góry na porażkę. Przeprowadzony przeze mnie eksperyment dostarczył mi najlepszego dowodu na to, że nie ma sensu implementować na siłę mechanizmów znanych z innych języków. PHP ma swoją naturę, która czasami jest ograniczeniem, ale często stanowi też jego zaletę.