Как стать автором
Обновить

Импорт фотографий из командной строки Linux

Время на прочтение 8 мин
Количество просмотров 5.6K

Собралось много фотографий на различных носителях, да еще в полном беспорядке. Где, когда было отснято, и как это все отсортировать и привести в порядок? Объемы большие, работы много. Сортировать все это вручную, так себе вариант. Использовать какие-то органайзеры и прочий софт желания не было. Тем более что простой просмотрщик уже был установлен. Но использовать его для сортировки занятие не только утомительное, но еще и затратное по времени и силам. Побродив немного по всемирной паутине в поисках решения, как это все сделать просто, да еще и в фоновом режиме и не найдя подходящего, для себя по крайней мере, решения, написал простенький скрипт на shell-е. Почему shell? Да все просто, его более чем достаточно для этой задачи. Плюс внешние утилиты командной строки, используемые в скрипте, в любом Linux идут по дефолту. Но это все предыстория, а нам пора переходить  к скрипту.

Что умеет скрипт

Представьте что у нас хранятся сотни тонн всевозможных снимков в совершенно разных местах, на разных носителях. Где-то они были разобраны по годам и месяцам, а где-то просто “свалены” в кучу и оставлены до лучших времен.

Мы запускаем программу указываем ей начальную точку, откуда начинать, и идем заниматься своими делами. Скрипт сканирует все директории какие встретит на своем пути, включая и вложенные, в поисках фотографий. После завершения работы программы получаем фотоальбом с отсортированными снимками по годам и месяцам в каждом году, т.е. в виде: год / месяц / снимки. Да еще и без дубликатов фотографий. А сами снимки будут переименованы из непонятных типа IMG_654372984.jpg или AB54645456.jpg во вполне читабельные и понятные имена вида YYYYMMDD_hhmmss.jpg. При этом не пострадает ни один оригинал фотографии.

сам скрипт import-photo
#! /usr/bin/env bash

# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
# Импорт фотографий в фотоальбом
# author: VlaSard
# github: https://github.com/VlaSard
# date: 2022.09.05
# photo-import
#
# DESCRIPTION:
#   Импортирует фотографии в фотоальбом и сортирует по директориям [YYYY / MM].
#   Переименовывает фотографии в формат YYYYMMDD_hhmmss.jpg.
#   НЕ УДАЛЯЕТ оригиналы файлов.
#   НЕ УВЕЛИЧИВАЕТ разрешение.
#
#   !!! convert - РАБОТАЕТ БЕЗ ПРОВЕРОК НА НАЛИЧИЕ ДУБЛИКАТОВ !!!
#
# REVIEWS:
#   exiv2
#   convert
#
# USAGE:
#   photo-import [dir_name]
#   dir_name - необязательный параметр, откуда будем импортировать фотографии
#
# -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

# -=-=-=-=-=-=-= VARIABLES =-=-=-=-=-=-=-
# если каталог с фотографиями не указан, используется текущий каталог
SourceDir=${1:-${PWD}}

# каталог назначения
DestDir="$HOME/Photo_album"

# настройка импорта фотографий:
# import - импорт без изменения фотографий;
# convert - импорт с изменением фотографий;
ImportPhoto=import

# степень сжатия фотографии
Quality=80

# максимальный размер фотографии
Resize=1920x1080

# переменные colors
NORMAL='\033[0m'
GREEN='\033[32m'
CYAN='\033[36m'
# RED='\033[31m'

# -=-=-=-=-=-=-= MESSAGES =-=-=-=-=-=-=-=-
msg_FileExists=("Файл" "уже есть в альбоме! ${CYAN}Пропускаем${NORMAL}.")
msg_FileImport=("Импортируем фотографию" "в альбом.")

# -=-=-=-=-=-=-= FUNCTIONS =-=-=-=-=-=-=-

# -== вывод сообщений скрипта ==-
# параметры:
#   ${message[@]} - сообщение в виде массива
#   ${file_name} - имя файла
msgPrint() {
    printf "%b\n" "${1} ${GREEN}${3}${NORMAL} ${2}"
}

# -== проверка наличия фотографии в альбоме ==-
# параметры:
#   $1 - объект источник
#   $2 - объект назначение
# возвращает: $FileExists
#   0 - объекта $2 нет
#   1 - объект $2 есть и он идентичен объекту $1
#   2 - объект $2 есть и он отличается от объекта $1
file_exists() {
    FileExists=0

    if [ -f "${2}" ]; then

        # если файлы одинаковые, пропускаем
        if cmp -s "${1}" "${2}"; then
            msgPrint "${msg_FileExists[@]}" "${1}"
            FileExists=1
            return
        fi

        FileExists=2
        return

    fi
}

# -== импорт фотографий без изменений ==-
# переменные:
#   $1 - находится полное имя фотографии в альбоме
#   FilePhoto - объект источник
#   destPhoto - объект назначения
#   baseName - имя файла без расширения
#   count - счетчик дубликатов фотографии
import() {
    destPhoto="${1}"
    local baseName="${destPhoto%.*}"
    local COUNT=1

    # проверить наличие фотографии в альбоме
    file_exists "${FilePhoto}" "${destPhoto}"

    # если фотография в альбоме есть - пропускаем
    if [ "${FileExists}" = 1 ]; then
        return 1

    # если фотография в альбоме есть, но отличается от исходной
    elif [ "${FileExists}" = 2 ]; then

        # проверяем все похожие фотографии
        while [ "${FileExists}" != 0 ]; do

            # добавили к имени счетчик и ушли на проверку
            NewName="${baseName}-${COUNT}.jpg"
            COUNT=$((COUNT + 1))
            file_exists "${FilePhoto}" "${NewName}"

            if [ "${FileExists}" = 1 ]; then
                return 1
            fi

        done

        destPhoto="${NewName}"

    fi

    # если фотографии в альбоме нет - импортируем
    msgPrint "${msg_FileImport[@]}" "${destPhoto}"
    cp -up "${FilePhoto}" "${destPhoto}"
}

# импорт с изменением фотографий
# переменные:
#   $1 - находится полное имя фотографии в альбоме
#   FilePhoto - объект источник
#   destPhoto - объект назначения
convert() {
    destPhoto="${1}"

    # если фотография есть в альбоме - пропускаем
    # if ! file_exists "${FilePhoto}" "${destPhoto}"; then
    #     return 1
    # fi

    # изменить качество и размер фотографии, переименовывать и копировать в альбом
    msgPrint "${msg_FileImport[@]}" "${FilePhoto}"
    convert -quality "${Quality}" -resize "${Resize}" "${FilePhoto}" "${destPhoto}"

}

# -=-=-=-=-=-=-=-= MAIN  =-=-=-=-=-=-=-=-

# перебираем все jpg-файлы в указанной директории и всех поддиректориях
find "${SourceDir}" -iname "*.jpg" | sort |
    while read -r FilePhoto; do

        for PhotoDate in "Exif.Photo.DateTimeOriginal" "Exif.Image.DateTime"; do

            # читаем дату из EXIF
            PhotoDate=$(exiv2 -g "${PhotoDate}" -Pv "${FilePhoto}")

            # если прочитали дату снимка, прекращаем поиск даты
            [ -n "${PhotoDate}" ] && break

        done

        # если не нашли в EXIF ищем в названии файла
        if [ -z "${PhotoDate}" ]; then

            # ищем дату в названии файла и формируем в виде YYYY:MM:DD HH:MM:SS для корректного добавления в EXIF
            PhotoDate=$(
                basename "${FilePhoto}" ".jpg" |
                    perl -e 'if
                    (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s)
                    { print "$1:$2:$3 $4:$5:$6" }'
            )

            if [ -n "${PhotoDate}" ]; then

                # если нашли добавляем дату из названия файла в EXIF
                exiv2 -M "add Exif.Image.DateTime Ascii ${PhotoDate}" "${FilePhoto}"

            else

                # если даты в названии не нашли, берем дату создания (изменения) файла
                PhotoDate=$(date +"%Y:%m:%d %T" -r "${FilePhoto}")

            fi

        fi

        # формируем массив с переменными для фотоальбома
        # AlbumParam[0] - адрес фотоальбома
        # AlbumParam[1] - новое имя файла фотографии
        # AlbumParam[2] - дата создания в соответствии с EXIF
        IFS=" " read -r -a AlbumParam <<<"$(echo "$PhotoDate" |
            perl -e 'if
            (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s)
            { print "$1/$2 $1$2$3_$4$5$6.jpg $1$2$3$4$5.$6" }')"

        # создать структуру фотоальбома (YYYY / MM)
        [ -d "${DestDir}/${AlbumParam[0]}" ] || mkdir -p "${DestDir}/${AlbumParam[0]}"

        # если импортировали фотографию
        if ${ImportPhoto} "${DestDir}/${AlbumParam[0]}/${AlbumParam[1]}"; then

            # установить дату создания в соответствии с датой в EXIF
            touch -t "${AlbumParam[2]}" "${destPhoto}"

        fi

    done

Под капотом

Подготовительные операции

Как уже упоминалось ранее при запуске мы указываем скрипту откуда начинать импортировать фотографии, начальную точку. Если этого не сделать, что допустимо, он сам определит в какой директории он был запущен и начнет оттуда. Директория фотоальбома задается только в теле скрипта, вряд ли она так часто меняется, чтобы передавать ее через командную строку. При желании нужно изменить всего пару строк. За все это отвечают строки приведенные ниже. В SourceDir присваивается или директория переданная через командную строку или директория в которой был запущен скрипт.

SourceDir=${1:-${PWD}}
DstDir="$HOME/Phot_album"

А дальше начинаем читать файл за файлом и раскладывать по директориям в альбоме, попутно создавая их если не было.

Ищем и читаем только jpg файлы. Поскольку это основной формат для хранения фотографий. Строки отвечающие за поиск и чтение файлов приведены ниже:

find "${SourceDir}" -iname "*.jpg" | sort |
	while read -r FilePhoto; do

	...

	done

Прочитав файл, пытаемся определить дату снимка из параметров EXIF. Циклом перебираем два тега и пытаемся прочитать из них информацию. Нашли, отлично. Пропускаем оставшиеся варианты поиска даты, проверив наличие параметра в переменной PhotoDate. Если их там не нашлось ищем в названии файла. При этом используется регулярка Perl т.к. она лучше документирована, позволяет сразу сформировать нужный результат, да и занимает всего одну строчку кода. В отличие от shell-овского варианта где пришлось бы использовать grep для поиска и sed для формирования результата. Если же в названии даты не нашлось возьмем дату создания (изменения) файла. Код выполняющий все описанные действия:

чтение даты
for PhotoDate in "Exif.Photo.DateTimeOriginal" "Exif.Image.DateTime"; do

	# читаем дату из EXIF
	PhotoDate=$(exiv2 -g "${PhotoDate}" -Pv "${FilePhoto}")

	# если прочитали дату снимка, прекращаем поиск даты
	[ -n "${PhotoDate}" ] && break

done

# если не нашли в EXIF ищем в названии файла
if [ -z "${PhotoDate}" ]; then

	# ищем дату в названии файла и формируем в виде YYYY:MM:DD HH:MM:SS для корректного добавления в EXIF
	PhotoDate=$(
		basename "${FilePhoto}" ".jpg" |
			perl -e 'if 
			(<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s)
			{ print "$1:$2:$3 $4:$5:$6" }'
	)

	if [ -n "${PhotoDate}" ]; then

		# если нашли добавляем дату из названия файла в EXIF
		exiv2 -M "add Exif.Image.DateTime Ascii ${PhotoDate}" "${FilePhoto}"

	else

		# если даты в названии не нашли, берем дату создания (изменения) файла
		PhotoDate=$(date +"%Y:%m:%d %T" -r "${FilePhoto}")

	fi

fi

Отлично. Дату нашли. Теперь формируем необходимые переменные для создания структуры фотоальбома. Этим также займется perl-однострочник. Сформируем сразу все переменные, у нас их три, и сложим в массив. Bash умеет неплохо работать с массивами.

IFS=" " read -r -a AlbumParam <<<"$(echo "$PhotoDate" | perl -e 'if (<> =~ /(\d{4})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})[-_:\ ]?(\d{2})/s) { print "$1/$2 $1$2$3_$4$5$6.jpg $1$2$3$4$5.$6" }')"

В shell-е пришлось бы написать подобную конструкцию в четыре строки кода. Разобрать дату на составляющие. А уже потом в каждой строке формировать три переменные. Здесь же одна строка кода.

В строках:

if ${ImportPhoto} "${DestDir}/${AlbumParam[0]}/${AlbumParam[1]}"; then
	touch -t "${AlbumParam[2]}" "${destPhoto}"
fi

запускаем процедуру импорта фотографии и установки даты создания файла в соответствии с датой в EXIF. Если ничего не импортировали, то ничего и не меняем.

Импорт снимка

За импорт фотографии отвечает функция import, код функции ниже:

функция import()
import() {
    destPhoto="${1}"
    local baseName="${destPhoto%.*}"
    local COUNT=1

    # проверить наличие фотографии в альбоме
    file_exists "${FilePhoto}" "${destPhoto}"

    # если фотография в альбоме есть - пропускаем
    if [ "${FileExists}" = 1 ]; then
        return 1

    # если фотография в альбоме есть, но отличается от исходной
    elif [ "${FileExists}" = 2 ]; then

        # проверяем все похожие фотографии
        while [ "${FileExists}" != 0 ]; do

            # добавили к имени счетчик и ушли на проверку
            NewName="${baseName}-${COUNT}.jpg"
            COUNT=$((COUNT + 1))
            file_exists "${FilePhoto}" "${NewName}"

            if [ "${FileExists}" = 1 ]; then
                return 1
            fi

        done

        destPhoto="${NewName}"

    fi

    # если фотографии в альбоме нет - импортируем
    msgPrint "${msg_FileImport[@]}" "${destPhoto}"
    cp -up "${FilePhoto}" "${destPhoto}"
}

Здесь все достаточно просто. В функцию передается только имя файла назначения. Прежде всего нужно проверить существует или нет снимок в альбоме, поручим это самостоятельной функции file_exists. Функция будет возвращать три значения проверки:

  • 0 - файла нет;

  • 1 - файл есть и он идентичен файлу источнику

  • 2 - файл есть но отличается от источника

С первыми двумя вариантами (0, 1) все просто или пропускаем или импортируем. В случае возврата в результатах проверки 2, просто перебираем имена, добавляя индекс к имени файла и отправляя на повторную проверку. Как только определим, что снимка с таким именем в альбоме нет производим импорт фотографии.

функция file_exists()
file_exists() {
    FileExists=0

    if [ -f "${2}" ]; then

        # если файлы одинаковые, пропускаем
        if cmp -s "${1}" "${2}"; then
            msgPrint "${msg_FileExists[@]}" "${1}"
            FileExists=1
            return
        fi

        FileExists=2
        return

    fi
}

Проверили наличие файла и если файл существует сравнили по содержимому файл источник и файл назначения и вернули соответствующее значение.

Полный текст скрипта приведен выше. Также он доступен на GitHub по ссылке.

Эпилог

Скрипт появился давно, но только в последнее время появилась возможность и желание навести немного порядка в библиотеке приложений.

Почему скрипт на bash, а не python к примеру? Хотя бы потому, что пришлось бы установить пакет для работы с тегами фотографий. В bash он присутствует по умолчанию. Да и задача не настолько тяжелая чтобы решать на других языках.

Единственное, что не прикрутил к нему конвертер снимков, для уменьшения размера файла. Да вряд ли такой функционал будет востребован. Но заготовку функции в скрипт вставил. А поскольку у него только одна функция импорта работает то и парсера командной строки тоже не стал добавлять. Также как нет и справки по работе с программой. Все настройки внутри скрипта в самом начале ровно как и небольшой туториал по нему.

Теги:
Хабы:
+7
Комментарии 16
Комментарии Комментарии 16

Публикации

Истории

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн