Конечно же, формат JPEG не поддерживает прозрачность, но сама идея использовать JPEG вместо PNG для прозрачных текстур будоражит умы довольно давно. Камрад PaulZi не так давно предложил использовать для HTML формат SVG, в котором хранится само изображение и маска. Jim Studt предлагает использовать EXIF поля в JPEG и хранить там маски, а отображать на веб-странице с помощью Canvas.
Оба метода относительно сложны для использования, да и рассчитаны на веб, потому я остановился на самом простом варианте: хранить отдельно lossy JPEG для RGB и lossless маску в PNG, а совмещать их на этапе получения UIImage в программе. Сразу хочу сказать, что пишу на MonoTouch, потому код привожу на C#, хотя в ObjC это делается почти точно так же, с учетом синтаксиса.
Разделение каналов
Для разделения я использую консольные утилиты ImageMagik.
Эта команда отделяет альфа-канал:
convert file.png -channel Alpha -separate file.mask.png
Следующая команда создает JPEG файл, отбрасывая данные о прозрачности. Что характерно, некоторые другие утилиты (в т.ч. и Photoshop) при конверсии PNG файла в JPEG добавляют к нему некую одноцветную подложку и лишь затем сохраняют в RGB, что дает красивую, но неверную картинку с pre-multiplied alpha.
convert file.png -quality 90 -alpha off file.jpg
Качество полученного файла регурируется параметром quality 90. 90% качества для JPEG это больше, чем Apple ставит для скриншотов программ и фильмов. Думаю, каждый сможет подстроить под свой вкус это значение.
Оптимизация
Маска получается в виде 8-битного Grayscale PNG без прозрачности. Такой формат отлично сжимается через optipng или через веб-сайт www.tinypng.org.
С JPEG ситуация интереснее. Можно было бы ограничиться только заданием качества, но недавно мне попалась замечательная утилита jpegrescan от Loren Merritt, одного из разработчиков ffmpeg и кодировщика x264 (на счет него же есть подозрения, что он является представителем инопланетного разума или кибернетического
Утилита использует необычный подход: подбирает разные коэффициенты для Progressive сжатия и выбирает наиболее оптимальные. Выигрышь получается от 5 до 15% с идентичным качеством картинки. Собственной страницы у утилиты нет, только топик с обсуждением и сам perl-код: pastebin.com/f78dbc4bc
Чтобы не вводить команды каждый раз вручную, я написал простенький скрипт на bash:
#!/bin/bash
basefile=${1##*/}
maskfile="${basefile%.*}.mask.png"
jpegfile="${basefile%.*}.jpg"
convert $1 -channel Alpha -separate $maskfile
convert $1 -quality 90 -alpha off $jpegfile
optipng $maskfile
jpegrescan $jpegfile $jpegfile
Вот результат работы скрипта:
В моем случае из файла в 1,8Мб получилось два файла на 380Кб и 35Кб.
Склеивание
Само склеивание делается очень просто — загружается две картинки в UIImage, затем создается на их основе CGImage методом WithMask (в ObjC это CGImageRef и initWithMask соответственно), а потом оборачивается в новый UIImage.
UIImage result;
using(UIImage uiimage = UIImage.FromFile(file))
using(UIImage mask = UIImage.FromFile(maskFile))
{
CGImage image = uiimage.CGImage;
image = image.WithMask(mask.CGImage);
result = UIImage.FromImage(image, uiimage.CurrentScale, uiimage.Orientation);
}
В реальном проекте я сделал чуть сложнее и проверяю наличие файла *.mask.png и если он отсутствует, то возвращаю обычный UIImage.FromFile().
Профит
Визуально игра не изменилась никак. Задержка загрузки и склеивания текстур на глаз не заметна, потому я и не стал ее замерять. Сам же проект уменьшился на 6 (!!) мегабайт, как в .ipa виде, так и в iTunes и на устройстве.
Скрин из игры в PNG. Никаких артефактов или проблем сжатия/прозрачности не видно.
Немного смущает удвоенное количество картинок в папке проекта, но это можно пережить. Изменения кода минимальные. Для графики интерфейса этот метод не идеален из-за необходимости вручную присваивать UIImage, а не загружать из NIB/XIB, но для собственных контролов или текстур подходит вполне. Даже если JPEG сохранять с 100% качеством, размер полученных файлов может быть меньше, чем изначального PNG без потерь качества.
P.S. ImageMagick и optipng ставятся из портов (MacPorts/Fink/Brew) и называются так же. Скрипт jpegrescan качается с pastebin и использует порт jpeg
Автор: Nomad1