Pull to refresh

Сортировка изображений по разрешению… на сцене PowerShell

Reading time 9 min
Views 8.6K
В очередной раз просматривая интересности, любопытности и прочие всякости на Хабре, натолкнулся на статью о том, как с помощью Питона навести порядок среди иллюстраций, цифровое кладбище которых имеется почти у каждого из нас. Поскольку не так давно мне выпала доля заниматься процессингом картинок с использованием PowerShell, я решил провести показательное сравнение. Показательное с той точки зрения, чтобы продемонстрировать некоторые характерные возможности PowerShell тем, кто еще пока не знаком с ним или знаком поверхностно.

К сожалению, с PowerShell сложилась странная ситуация, когда весьма мощный инструмент оказывается обойден вниманием общественности и определенно нуждается в некоторой популяризации. Тем более, что с недавнего времени он входит в составе Windows 7 и скоро будет на рабочих местах немалого количества пользователей. А тут такой повод в виде лаконичной с одной стороны, но интересной с другой задачи административного характера по наведению порядка в хранилищах информации. Итак, приступим.

Начну с небольшого лирического отступления. Даже когда говоришь о таких, вроде бы простых инструментах, как командные процессоры, хочется чего-то возвышенного. И мне кажется, я его нашел. Вы будете смеяться, но я определил для себя разработку под командные процессоры как мультипарадигменную. Первая парадигма — императивная. Мы видим ее практически во всех командных файлах и видели ее в примере исходной задачи на Питоне. Другая парадигма — функциональная. Я ее так назвал в силу ее схожести с подходом, используемым в функциональных языках программирования. В быту же мы ее знаем как командные пайпы, просто пайпы и много других ласковых терминов :) Вкратце напомню, как это выглядит, на простом примере:

X:\> (dir /b folder1\*.txt && dir /b folder2\*.txt) | find "text" | sort

Здесь мы видим три инструкции, разделенных вертикальной чертой. Каждая последующая берет результаты выполнения предыдущей, выполняет над ними определенные операции и передает следующей команде в очереди пайпа. В нашем примере стандартный cmd.exe собирает при помощи первой инструкции список текстовых файлов из двух папок. Этот список передается команде find, которая оставляет только те строки, которые содержат подстроку «text» и уже они отправляются команде sort, которая их сортирует. В функциональных языках программирования это могло бы выглядеть так:

sort(find((dir /b folder1\*.txt && dir /b folder2\*.txt), "text"))

Не правда ли, есть определенные сходства? По сути, каждый элемент пайпа сродни функции. Просто к существующим типам записей функций (как то постфиксная, префиксная и инфиксная записи) добавилась еще одна — пайповая запись :)

Не смотря на то, что командные пайпы, как видно из примера, были еще со времен MS-DOS, я бы хотел отдельно поблагодарить UNIX-сообщество, что звучит странно, учитывая происхождение PowerShell в горнилах Microsoft. Но тому есть простое объяснение. Именно в UNIX-подобных системах данные механизмы были возведены в ранг искусства, позволяя объединять различные команды в самые необычные и весьма полезные комбинации.

Так получилось, что PowerShell почерпнул именно эту, на мой взгляд очень удачную черту и объединил с другим, не менее удачным решением. В отличие от передачи строк в командных пайпах того же Linux, PowerShell оперирует объектами. Чтобы все стало понятней, предлагаю приступить к реализации поставленной задачи. Делать мы это будем, разумеется, с использованием «функционального» подхода через пайпы, ибо императивный подход отличался бы мало от того, что мы уже имеем в случае с реализацией на Питоне. А хочется сравнить не только инструменты, но и парадигмы.

В силу функциональности нашего подхода мы разобьем решение задачи на некоторое подобие этих самых функций, а в нашем случае — инструкций, каждая из которых выполняет какую-то свою строго определенную роль. Отдельно отмечу, что я буду намеренно немного усложнять реализацию, чтобы показать больше возможностей PowerShell. В реальных условиях что-то можно упростить, а от чего-то вообще избавиться.

Шаг 0. Для начала мы опишем некоторые условия исполнения в целом. Во-первых — нас интересуют входные параметры и это хороший повод рассмотреть работу с переменными в PowerShell.

PS X:\> $source="x:\folder\source"
PS X:\> $target="x:\folder\target"
PS X:\> $source, $target
x:\folder\source
x:\folder\target


В первой строке переменной $source мы определили в качестве значения исходную папку, в которой расположены картинки для последующей сортировки. В папке назначения $target мы разместим отсортированные картинки. Третья строка просто говорит о том, что надо вывести значения этих переменных на консоль, что мы и видим далее. Заметьте, что значения не просто так взяты в кавычки. Дело в том, что значения переменных типизированы и таким образом мы определили их как строки. В отсутствие кавычек процессор будет рассматривать текст как команду и присвоит переменной результаты ее исполнения. Например:

PS X:\> $test=dir x:\folder\source
PS X:\> $test.Length

10

PS X:\> $test[0].GetType()

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True True DirectoryInfo System.IO.FileSystemInfo


В результате такой команды переменная $test будет коллекцией объектов в заданной папке. Свойство Length, которые мы использовали в инструкции $test.Length — это количество элементов в коллекции. А вот $test[0].GetType() выводит информацию о типе первого элемента коллекции. Как видите, это не простая строка, а некий DirectoryInfo. Будь первым элементов файл — был бы FileInfo. Это очень важная иллюстрация к тому, что я говорил ранее и тому, что мы будем активно использовать позднее — PowerShell передает по пайпу не строки, а объекты вполне определенных типов.

Следующий подготовительный шаг связан с тем, что мы будем использовать тип иллюстрации, который размещен в библиотеке System.Windows.Forms, не загружаемой по умолчанию. Нам необходимо дать инструкцию PowerShell, чтобы он ее загрузил. Например, так:

PS X:\> [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")

Вообще здесь проглядывается еще одна существенная особенность PowerShell — это создание любых объектов, которые предлагает .NET или COM. Имя им легион, но это уже отдельная тема. В данном случае просто примем данную строку как данность. За сим будем считать, что среда исполнения готова.

Шаг 1. После того, как условия исполнения задачи готовы, мы начнем с формирования списка файлов, которые претендуют на то, чтобы быть картинкам. Формулировка задачи именно такова, ибо мы исходим из предположения, что расширение файла — это полезная информация, но не гарантирующая того, что файл является картинкой. В большинстве случаев вам это не понадобиться, но как я говорил ранее — решение намеренно усложнено. Итак, мы выбираем все файлы заданных расширений в заданной папке и всех ее подпапках. Выглядеть наш первый шаг пайпа будет так:

PS X:\> dir $source -r -include *.jpg, *.png, *.gif

Опция "-r" означает рекурсивный обход директорий, в "-include" вы можете перечислить маски включаемых файлов (либо перечислить макси исключаемых в опции "-exclude"). В ответ на эту команду мы получим список файлов.

Шаг 2. Следующим элементом пайпа мы пытаемся создать для каждого файла, полученного от предыдущей инструкции, объект растровой картинки. Данная инструкция служит иллюстрацией сразу нескольким возможностям PowerShell, но начнем, для ясности картины, с примера:

PS X:\> dir $source -r -include *.jpg, *.png, *.gif | select FullName, @{Name="Image"; Expression={New-Object System.Drawing.Bitmap $_.FullName}} -ErrorAction SilentlyContinue

Первое, что мы видим во второй инструкции пайпа — это команду select. Ее назначение состоит в том, чтобы сформировать новые объекты и передать их дальше. Для этого в select через запятую перечисляются все свойства нового объекта, которые нас интересуют. Первым идет FullName. Указанное в таком виде свойство означает, что мы берем его из объекта, доставшегося нам по пайпу и с тем же именем и значением передаем новому объекту. В нашем случае речь идет о свойстве FullName класса FileInfo, которое возвращает полный путь к файлу.

Следующая конструкция немного сложнее. Она создает новое свойство, имя которого передается в Name, а значение в Expression. В качестве значения мы создаем экземпляр класса, описывающего иллюстрацию (System.Drawing.Bitmap), передавая его конструктору тоже самое значение FullName с расположением файла иллюстрации. Отдельно отметьте для себя разницу в синтаксисе обращения к свойству FullName. Инструкция select делает это в упрощенном виде. В большинстве же остальных случаев переменная $_ означает объект, переданный нам по пайпу, к свойству которого мы можем обратиться через точку и имя свойства.

Если файл, с которым мы собираемся работать не является растровой иллюстрацией, то попытка создания объекта System.Drawing.Bitmap привела бы к ошибке. Для того, чтобы эти ошибки проигнорировать мы добавили опцию ErrorAction, которая позволяет их проигнорировать. Отметьте для себя, что эта опция не является уникальной для команды select, а относится к разряду так называемых Common Parameters, которые вы можете использовать практически в любых других инструкциях.

Шаг 3. По итогам предыдущего шага мы получим список объектов, каждый о двух свойствах: FullName с полным путем к имени файла и Image с иллюстрацией в виде экземпляра класса Bitmap. Если для какого-либо из файлов не удалось создать класс растровой картинки, то свойство Image будет пустым. Значит нам нужен шаг, который позволить отфильтровать все объекты, которые не являются иллюстрациями. Итог дополнения новой инструкцией будет такой:

PS X:\> dir $source -r -include *.jpg, *.png, *.gif | select FullName, @{Name="Image"; Expression={New-Object System.Drawing.Bitmap $_.FullName}} -ErrorAction SilentlyContinue | where { $_.Image }

Все достаточно просто и лаконично. Мы встречаем знакомое нам обращение к свойству. В данном случае к свойству Image. Встречаем новую инструкцию where, которая позволяет передать дальше по пайпу только те объекты, которые удовлетворяют заданному условию в ней условию. Заодно знакомимся с простой проверкой на пустые значения. Непустое мы бы контролировали условием !$_.Image, а к более сложным условиям бы привлекали операции сравнения, логические операции и т.п. Например — where {$_.Image.Width -gt 1000 -and $_.Image.Height -gt 1000} для получения всех иллюстраций чьи ширина и высота больше 1000.

Шаг 4. После того, как мы получили перечень иллюстраций мы сформируем для каждой из них имя папки, в которой иллюстрация должна быть сохранена. Сделаем мы это так:

PS X:\> dir $source -r -include *.jpg, *.png, *.gif | select FullName, @{Name="Image"; Expression={New-Object System.Drawing.Bitmap $_.FullName}} -ErrorAction SilentlyContinue | where { $_.Image } | select FullName, @{Name="ImageFolder"; Expression={"{0}\{1}x{2}" -f $target, $_.Image.Width, $_.Image.Height}}

С командой select и формирование нового объекта вы уже знакомы, больший интерес здесь представляет форматирование строк. Как видно из примера, начинается все со строки с форматом, а затем перечисляются значения, которые будут использованы при форматировании. Все в большей мере соответствует методу string.Format из .NET и с правилами форматирования можно ознакомиться в MSDN.

Шаг 5. Посмотрев на результат исполнения этой функции вы увидите, что уже есть практически все, что нам нужно. А именно — мы имеем полный путь к исходной иллюстрации все в том же свойстве FullName и путь назначения с папками согласно размерам в новом свойстве ImageFolder. Остался сплошной императив по созданию папки и копированию/перемещению туда файла. Для этого мы воспользуемся инструкцией foreach, которая позволяет выполнить другие инструкции для каждого объекта, полученного в пайпе. Выглядеть все вместе это будет так:

PS X:\> dir $source -r -include *.jpg, *.png, *.gif | select FullName, @{Name="Image"; Expression={New-Object System.Drawing.Bitmap $_.FullName}} -ErrorAction SilentlyContinue | where { $_.Image } | select FullName, @{Name="ImageFolder"; Expression={"{0}\{1}x{2}" -f $target, $_.Image.Width, $_.Image.Height}} | foreach {if (-not (test-path $_.ImageFolder)) {md $_.ImageFolder}; copy $_.FullName -destination $_.ImageFolder; $_}

Как видите, в foreach разместились три инструкции, разделенные точкой с запятой. Вторая определенно не нуждается в каких-либо пространных комментариях, ибо представляет из себя простое копирование. Которое, к слову, может быть заменено на команду move для перемещения файла иллюстрации. Первая инструкция чуть длиннее, но не намного сложнее. В условной конструкции if мы проверяем отсутствие папки при помощи логического отрицания и конструкции test-path. Если папка отсутствует, то только в таком случае мы ее создаем. Третьей инструкции можно было вовсе избежать, но ей я хотел показать, что foreach не является терминальной инструкцией в пайпе и после нее обработка может быть продолжена. Помните, как на самом первом шаге мы выводили значения переменных на консоль? Так и здесь, инструкция $_ выводит объект, который мы получили по пайпу дальше в пайп. Вместо него вы можете вывести что-либо другое. Например, определить какую-нибудь переменную и вывести ее, скажем foreach {….; $result = …; $result}.

Подводя итог, эти несколько коротких строчек — есть чуть более нагроможденный вариант решения задачи. Хорошо ли это, плохо ли — мне сложно судить. Наверно я здесь и затем, в том числе, что добавить в свою копилку толику вашей коллективной мудрости :)

UPD: Огромное спасибо amirul за пример в императивном стиле, который я сделать поленился. Надеюсь это снимет некоторые проблемы с читаемостью кода. Хотя, не скрою, хочется чтобы был понят и функциональный подход. Он ничуть не сложнее, ведь замысловатая с виду строка проходит элементарную декомпозицию на примитивные атомы. Просто это непривычно, как непривычен многим из нас синтаксис LISP, например.

Copy Source | Copy HTML
  1. [void][reflection.assembly]::LoadWithPartialName("System.Windows.Forms")
  2.  
  3. $source = "x:\source"
  4. $target = "x:\target"
  5.  
  6. foreach ($file in dir $source -r -inc *.jpg, *.gif, *.png) {
  7.     try {
  8.         $image = new-object System.Drawing.Bitmap $file.FullName
  9.         $targetdir = "{0}\{1}x{2}" -f $target, $image.Width, $image.Height
  10.         if (!(test-path $targetdir)) {
  11.             md $targetdir
  12.         }
  13.         copy $file $targetdir
  14.  
  15.         Write-Host $file -> $targetdir
  16.     } catch {
  17.         Write-Host $file " **IS NOT COPIED**"
  18.     }
  19. }
  20.  
Tags:
Hubs:
If this publication inspired you and you want to support the author, do not hesitate to click on the button
+32
Comments 74
Comments Comments 74

Articles