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:

Przeskalowanie obrazków w katalogu

Pracując nad funkcjonalnością uploadu plików z możliwością ich wcześniejszego podglądu postanowiłem, że pliki nie będące obrazkami będą prezentowane za pomocą ikon odzwierciedlających ich rozszerzenie.

Ściągnąłem sobie z deviantarta [zestaw przykładowych ikon](http://fc07.deviantart.net/fs30/f/2008/064/4/5/4532f3c75d996fa7.rar). Bardzo fajne, kolorowe i duże obrazki. Stanąłem przed potrzebą przeskalowania wszystkich ikonek. Pierwsza myśl to poszukać przeglądarki zdjęć z możliwością masowej edycji plików. Chwilę później przypomniałem sobie, że mam zainstalowanego image magicka. 5 min i miałem już napisany prosty skrypcik w bashu, który tworzy podkatalog na miniatury, wyszukuje w bieżącym katalogu wszystkie pliki o zadanym rozszerzeniu i tworzy ich przeskalowane kopie.

#!/bin/sh
WIDTH=100
HEIGHT=50
EXT=png
THUMBDIR=./thumbnails-${WIDTH}x${HEIGHT}
 
mkdir $THUMBDIR
 
for IMG in `ls *.${EXT}`
do
	convert -resize ${WIDTH}x${HEIGHT} $IMG ${THUMBDIR}/$IMG
done

Automatyczna kopia bezpieczeństwa

Bezpieczeństwo danych to sprawa kluczowa. Jednym z ważniejszych zabiegów jest systematyczne tworzenie kopii bezpieczeństwa czyli tzw. backup. W ramach ćwiczeń w bashu postanowiłem napisać sobie taki mały skrypcik, który po odpaleniu przekopiuje cały katalog domowy mojego użytkownika na zewnętrzny dysk USB.

#!/bin/sh
 
#ustawienia sciezek
DEST_PATH="/path/to/my/external/disk"
BACKUP_DIR=$DEST_PATH/$(date +%Y-%m-%d-%H:%M)
SOURCE_PATH="/home/myuser"
 
#ustawienia domyslne
MODE="full"
TYPE="tar"
 
#wyswietlenie informacji jezeli bedzie mniej niz 2 argumenty.
if [ $# -lt 2 ]; then
    echo "Usage: `basename $0` <TAR|COPY> <FULL|incremental X days ago>"
fi
 
#jezeli podany jest drugi argument w linii polecen, to przystap do analizy
if [ ! -z "$2" ]; then
    case "$2" in
        FULL|full)
            MODE=full
            ;;
        *)
            MODE=$2
            ;;
    esac
fi
 
#jezeli pierwszy argument nie jest pusty do nalezy go przeanalizowac
if [ ! -z "$1" ]; then
    case "$1" in
        TAR|tar)
            TYPE=tar
            ;;
        copy|COPY)
            TYPE=copy
            ;;
    esac
fi
 
#jezeli katalog docelowy backupu nie istnieje, to nalezy go stworzyc
if [ ! -e $BACKUP_DIR ]; then
    mkdir -p $BACKUP_DIR;
fi
 
#funkcja robiaca backup za pomoca tara
make_tar_backup ()
{
    if [ "$MODE" = "full" ]; then
        echo "Full backup as archive TAR"
        tar -zcp -f $BACKUP_DIR/full.tgz $SOURCE_PATH
    else
        echo "Incremental backup as archive TAR. $MODE days ago"
        tar -zcp --newer-mtime "$MODE days ago" -f $BACKUP_DIR/incremental_$MODE.tgz $SOURCE_PATH
    fi
}
 
#funkcja robiaca backup za pomoca cp
make_copy_backup ()
{
    if [ "$MODE" = "full" ]; then
        echo "Full backup as save copy"
        for source_files in $SOURCE_PATH; do
            echo -e "\tDirectory $source_files"
            cp --parents -a $source_files $BACKUP_DIR
        done
    else
        echo "Incremental backup as save copy. $MODE days ago"
        for s in $SOURCE_PATH; do
            items_list=`find $s -ctime -$MODE`
            echo -e "\tDirectory $s"
            if [ ! -z "$items_list" ]; then
                for item_file in $lista; do
                    cp --parents -a $item_file $BACKUP_DIR
                done
            fi
        done
    fi
}
 
#start pomiaru czasu
time_start=$(date +%s);
 
#w zaleznosci od sposobu przechowywania backupu, uruchamiamy odpowiednie funkcje.
case "$TYPE" in
   tar)
      make_tar_backup
      ;;
   copy)
      make_copy_backup
      ;;
esac
 
time_end=$(date +%s);
exec_time=`expr $time_end - $time_start`
echo "Backup successfull. Execution time $exec_time seconds"

Proste ale działa 🙂

Bash-owe początki

Rzadko sięgam do Bash-a i zawsze, kiedy to robię muszę sobie wszystko przypominać od początku. No dobra – nie wszystko ;). Dzisiaj przeglądałem dysk i natknąłem się na mały programik, który kiedyś napisałem tak przy okazji. Pracowałem wtedy nad pewnym spider-em w pythonie i w ramach testów musiałem często kill-ować uruchomione przez tego „pajęczaka” procesy. Natchnęło mnie to wtedy do napisania prostego skryptu „killbyname” :D. Pamiętam, że jak na tak prostą funkcjonalność zajęło mi to wtedy zdecydowanie zbyt wiele czasu 😉

#!/bin/bash
# killbyname: Killing processes by name.
 
E_BADARGS=66
 
if test -z "$1"  # No command line arg supplied?
then
  echo "Usage: `basename $0` Process_to_kill"
  exit $E_BADARGS
fi
 
PROCESS_NAME="$1"
 
echo "Do you want kill processes:"
ps ax | grep "$PROCESS_NAME"
read -p "Yes (y) or No (n): " CONFIRM
 
if [ $CONFIRM = 'y' ]
then
    ps ax | grep "$PROCESS_NAME" | awk '{print $1}' | xargs -i kill {} 2&>/dev/null
fi
exit $?

W ramach ćwiczeń popełniłem też skrypt usuwający katalogi svn-a z projektu. Aż wstyd się przyznać ale pisząc to nie wiedziałem jeszcze, że istnieje coś takiego jak ***svn export***.

#!/bin/bash
# rmsvn
current_path=`pwd`
echo "Czy na pewno chcesz z katalogu $current_path \
 usunąć wszystkie podkatalogi .svn wraz z zawartością?"
echo "Potwierdź [y] lub anuluj [n]"
read confirmation
if [ $confirmation = 'y' ]
then
    find $current_path -name "\.svn" | xargs rm -rf
	if [ $? = 0 ]
	then
	    echo "Polecenie zostało wykonane."
    else
	    echo "Wykonanie polecenia się nie powiodło!"
	fi
else
    echo "Polecenie zostało anulowane."
fi

Jak już jesteśmy przy svn-ie to najczęściej okazuje się, że na serwerze produkcyjnym nie ma możliwości skorzystania z niego. Do tego jeszcze zdarzają się klienci kombinatorzy próbujący samodzielnie dokonywać zmian w projekcie. Aktualizacja plików w takiej sytuacji sprowadza się do ręcznego porównania katalogów z wersją live i dev. Kdiff ułatwia znalezienie plików różniących się od siebie, ale brakuje narzędzia ułatwiającego wyodrębnienie wyżej wspomnianych z całego projektu. Z tego właśnie powodu podjąłem się zadania napisania skryptu porównującego dwie wersje projektu i kopiujące do katalogu update tylko te pliki z nowej wersji, które różnią się od odpowiadających im plików starej wersji. Struktura katalogów zostaje zachowana. Programik jest bardzo prosty i bynajmniej nie niezawodny, ale przydał się kilka razy.

#!/bin/bash
# makeupdate
 
if [ $# -lt 2 || $1 = $2 ]
then
    echo "Usage: `basename $0` dir_with_old_version dir_with_new_vesion"
    exit
fi
 
OLD_DIR="$1"
NEW_DIR="$2"
 
UPDATE_DIR="./update"
mkdir $UPDATE_DIR
 
FILES_LIST=`diff -rqPx '.*' $OLD_DIR $NEW_DIR | awk '{print $4}'`
for FILE_PATH in $FILES_LIST
do
   DIR_PTH=`echo $FILE_PATH | sed -r 's/\/[a-Z0-9_]{1,}\.[a-z0-9]*//g'`
   mkdir -p $UPDATE_DIR/$DIR_PTH
   cp $FILE_PATH $UPDATE_DIR/$DIR_PTH
done
 
exit $?

Posted in Bash by Zbigniew Heintze · Tag: