Opened 4 years ago

Last modified 3 years ago

#938 new enhancement

Концепт модуля: Миниатюры как часть прогрессивных JPEG, PNG.

Reported by: 0x4E69676874466F78@… Owned by:
Priority: minor Milestone:
Component: nginx-module Version: 1.9.x
Keywords: Cc:
uname -a:
nginx -V: nginx/1.9.12

Description

Здравствуйте. Не знаю куда лучше написать по этому поводу поэтому пишу сюда, не хочу чтобы идея пропала. Возможно она нестостоятельная, но я на всякий случай напишу про неё.

Предложение сделать модуль отдачи прогрессивных картинок до определённого уровня. В изображениях прогрессивно закодированых есть грубо говоря уровни предпросмотра, которые вполне соответветствуют определённым размерам в пикселях. Суть в том чтобы отдавать изображение ровно до того уровня когда оно покрывает размер миниатюры, а дальше рвать соединение. Клиент получит часть изображения, но она корректно отобразится.
Тогда можно было бы забыть о генерации миниатюр и не тратить место под них. Возможен конечно небольшой перерасход трафика ибо ближайший покрывающий уровень может быть размером 500х500 а не 200х200, но это надо выяснять на большой коллекции изображений и можно в таких случаях брать ближайший прошлый уровень если он не сильно далеко от размера желаемой миниатюры.

Пример конфига:
location /thumb/ {

access_log off;
alias /var/www/site1/img/;
image_progressive 200 auto -50 300;

}
У разного размера картинок разные уровни будут соотвествовать разному размеру, поэтому указываем желаемый размер. Тут 200 ширина, а auto высота желаемой минаютюры. Auto автоматом просчитает размер высоты относительно ширины (сохранение соотношения сторон). -50 это даём понять что если ближайший уровень размером 150 пикселей то берём его, при условии что следующий уровень за 300 пикселей вперёд (то есть не 200, а 500). Но для начала можно и без этой замороченной логики.

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

Я погуглил и нашёл проект решающий проблему своим путём:
http://fhtr.org/multires/spif/spif.html
https://github.com/kig/multires
Но это не то, у него свой формат, а хотелось бы использовать уже существующие технологии.

C гифками вроде так тоже можно, но я не заметил подобной поддержки в обозревателях, так что для них здесь добавляется дополнительное условие перенаправляющее на сделанные миниатюры или задействовать image_filter resize.

Change History (3)

comment:1 Changed 4 years ago by 0x4E69676874466F78@…

А почему нельзя отредактировать свой тикет?

comment:2 Changed 3 years ago by xamica@…

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

Начнем с небольшой теории, как работают картинки вообще (и как я это помню, писать буду по памяти). Я рассмотрю форматы jpg+png+gif, каждый из которых я разбирал по байтикам и моя футболка пахнет настолько отвратительно, что любая баба просто бежит от меня. Дальше будет понятно, почему это хорошо и плохо.

Кодирование жопега представляет из себя мозаику из чернобелых квадратиков размером 8х8 пикселей. Не больше и не меньше. Если размер картинки не кратен 8х8, а скажем 500х500 пикселей, то НА САМОМ ДЕЛЕ КАРТИНКА ДОПОЛНЯЕТСЯ ДО КРАТНОСТИ дополнительными пикселями, А РЕПТИЛОИДЫ СКРЫВАЮТ, удаляя их при отображении на клиенте. Так как картинки у нас обычно цветные, да еще и в цветовом формате YUV, то можно сказать, что внутри картинки у нас 3 картинки в разных цветовых плоскостях: яркостная плоскость (просто чернобелая картинка) и 2 плоскости цветов. Так как человеческая тушка херово определяет цвета, в отличии от яркости, то были изобретены всякие YUV420 или YUV422, называется это разными мудреными словами вроде "хромасемплинг", без понимания, что это за слово вообще. На самом деле можно представить, что если у нас есть основная картинка 256х256 пикселей, то внутри нее будет чернобелая картинка 256х256 пикселей и еще 2 картинки по 128х128 пикселей, которые при декодировании просто растягиваются до 256х256. В теории, формат позволяет их растягивать не только в 2, но в 3 и 4 раза, но такого я в интернетах не встречал, да и велик шанс, что декодеры такое не отработают. В принципе, внутри может быть даже RGB, но на практике это почти не встречается. Так вот, если мы растягиваем цветовую составляющую в 2 раза, то декодированный квадратик будет уже 16х16 пикселей (максимум 32х32), хотя по прежнему содержит лишь 8х8 реальной информации. Поэтому, если мы хотим беспотерьно ебать жопеги, то надо иметь в виду, что безопаснее всего это делать, если операции трансформации кратны квадратикам 32х32, если мы ничего о нем не знаем.

Все 3 картинки внутри потока перемешаны друг с другом и образуют MCU - Minimal Coding Unit. Каждая из картинок, как уже было сказано - лишь квадратик 8х8 пикселей, представляющий собой набор частот, от просто залитого каким-то цветом низкочастотной информации (DC-коэфициент, самый первый и "наиболее весомый"), заканчивая информацией о тонких линиях - высокочастотной информацией, которую и въебывает алгоритм компрессии (AC-коэфициенты). Внутри этих квадратиков мы можем резать/клеить картинки относительно без потерь (лишь бы матрицы квантования были одинаковыми), декодить или делать с ними что-то еще. К примеру, мы можем выпиливать часть AC-коэфициентов и отдавать картинку дальше, как это делал ускорятор от Оперы, въебывая качество картинок, но ебически повышая скорость загрузки.

Прогрессивный жопег - это по сути то же самое, только сначала пачкой идут DC-коэфициенты от всех квадратиков, а потом пачки AC-коэфициентов, постепенно шаг за шагом улучшая картинку. Если даже насрать на то, что далеко не каждая картинка является прогрессивным жопегом, приняв каждый DC-коэфициент за 1 пиксель, мы таким образом сможем лишь уменьшить картинку в 8 раз по вертикали и в 8 раз по горизонтали...

И вот тут начинаются проблемы. Пользователи тебя будут за такое решение ненавидеть. Во-первых, им придется выделить память под полную картинку, а если там было в оригинале что-то вроде 10000х10000? Да, обрезав AC-коэфициенты, мы фактически можем скормить ему "превьюшку" 1250х1250 пикселей, но занимать у него в памяти оно будет 10000х10000 пикселей, скорее всего по 4 байта RGB32, скорее всего несколько буферов из-за рескейлера, так как если даже хоть на 1 пиксель картинку надо отресайзить - начинается достаточно долгий ресайз. В результате память по прежнему жрется как не в себя, все тормозит и пользователь готов оторвать голову за такую верстку. Впрочем, в 2017-м году это почти стало нормой и весь веб тормозит как хуй знает что. Но главное, что мы почти ничего не выиграли - мы фактически отдали картинку без компрессии, да еще ебического размера 1250х1250 пикселей. Ну и надо помнить о инвалидах, в каком-то браузере может случится "оппачки, соединение закрылось раньше времени, пичалька", вылезет эксепшен и только что показанная картинка будет заменена на "опачки, изображение содержит ошибки, я обкакалось".

Аналогичная ситуация с PNG, но тут картинка хотя бы сжата и реально будет отдана 1/64-картинки (по пикселям, не байтовому объему) до заполнения всего канваса. Впрочем, прогрессивных PNG я почти не встречал в природе.

Хуже всего дела обстоят с GIF, его черезстрочная развертка делает прореживание только по вертикали, потому если резануть картинку после первого скана, то мы лишь уменьшим разрешение в 8 раз по вертикали (но не горизонтали). Если у нас еще и анимированный GIF, то мы наверняка соснем с суб-пиксельным смещением, в результате анимация будет рассыпаться на части и выдавать кучу артефактов. Впрочем, этим грешит куча софта и это можно увидеть на куче сайтов.

В общем, магию о байтоебле картинок можно использовать, но лучше на клиенте ей не пользоваться (надеюсь, идея декодить картинки на тормозном JS не придет тебе в голову). Этим лучше заниматься на сервере, о чем я напишу чуть позже.

Теперь немного о кешировании, вставке рандомных байт и всем остальном. Чтобы узнать о том, что где-то там поменялось пара байт, то этот файл надо скачать целиком, а потом уже проверять, что там изменилось. Смысл кеширования пропадает. Потому для контроля кеша изобрели http-заголовки, среди которых Modified и Etag, которые позволяют узнать что там было примерно и стоит ли это выкачивать снова. Так как отрасль на 99% состоит из веб-обезьян, то не стоит полагаться на работу этих хрупких механизмов и делать версионирование.

Но проблема на самом деле гораздо шире. Дело в том, что зачастую требуется сделать несколько разных превьюшек: для списков, для галерей, для попапов. В последние годы пидорасы со своими ретинами засрали все и вполне можно встретить аватарку 16х16 пикселей, внутри которой будет 1600х1600. Этим ретинанутым нужно выдавать свои картинки. В общем, количество превьюшек резко росло.

А еще превьюшку сделать не так просто, как кажется. Ну, во-первых, кто-то может залить картинку 65535х65535 пикселей и обвалить сервак, выжрав всю оперативу. Весить же такая картинка может менее 1 килобайта. В былые времена такие картинки вставляли куда только можно, чем вызывали падения чего угодно, включая десктопного софта. Залить могут и вполне хорошую картинку, к примеру, с цилиндрической панорамой. Я люблю панорамы. А еще я люблю, когда превьюшка панорамы сжимается до 120х1 пиксель, по которой даже мышкой попасть сложно. Это если автор не подумал и не сделал обрезание картинки до некоторого прямоугольника. А если сделал - лица будут обрезаны по середине. Это можно решить применив нейросетку для поиска лиц. Выглядит смешно, но крупные ресурсы так и делают. Но даже обрезав нашу панораму корректно, многие любят открывать попап 640х480, а панорама без резки, вписанная в этот прямоугольник, будет высотой пикселей в 100. И получается, что на превьюшке видно больше, чем в попапе. Приходится открывать еще "оригинальное изображение" через какое-то контекстное меню, расположенное в жопе сайта. Я уже не говорю о том, что картинке желательно сделать нормализацию цвета, шарпенинг и тому подобное (что, между прочим, делает винда для отображения тумбинашек, потому тумбинашки выглядят отлично почти всегда). И кстати, это особенно касается видео или гиф-анимашек. При должном уровне аутизма можно генерировать 2-цветные гифки с достаточно большим количеством визуальной информации и небольшим весом. Знали бы вы, как больно лазить по порносайту, где есть только 1-кадровые превьюшки, не анимирующиеся при наведении на них мышки...

На этом закончим темную сторону...

Вот если же реализовать ресайз на сервере, генерить тумбинашки на лету, используя сложные алгоритмы... Вот это была бы ВЕЩЬ! Не нужно больше думать о том, какие тумбинашки и где хранить, о их пиксельном размере, пусть дизайнер сунет размер в шаблон вида /resize/1000x1000/img.jpg и система сама все сделает. Пусть дизайнер сам дрочится и делает сколько угодно версий для айфонов, айпадов и аймразей. Можно давать пользователю адаптивные картинки исходя из его разрешения экрана. И никаких файлов на реальной файловой системе! Все хранить в кей-велью подобной базе, где в качетсве ключа будет ID картинки + ее модификация + размер, сама база может быть прямо на блочном устройстве, как это делают всякие хайстеки, обычная файловая система вообще не нужна! Можно...

Но как уже было сказано выше, ресайз - штука затратная и как бы мы не байтоебили, какую бы магию байтоебли не использовали, все равно если нас попытаются выебать запросами вида /resize/1x100000/img.jpg, а этот сам img.jpg будет весить под 500 метров, то нам придется не сладко. Значит, придется ограничивать возможные разрешения. В принципе, можно сделать пре-процессинг шаблонов и все будет хорошо. Для адаптивного ресайза выбирать просто ближайший размер. Можно, в принципе, вызвать condition race, заваливая запросами на одну картинку, пока она ресайзится. Без злого умысла, если картинка эта в чате или в какой-то ленте с постоянным обновлением. Особенно сложно, если у нас распределенный бекенд и куда упадет следующий запрос - не очень понятно, как все это синхронизировать. А должен ли быть у всех инстансов общий кеш или распределенный? В общем, сложно все это.

Что же делать? В конечном итоге я просто выделял памяти под 200х200 пикселей и высерал туда писель за пикселем, корректируя его положение по некоторому коэфициенту. Из своего собственного декодера. Получался ресайз без ресемплинга (nearest neighbor), что в общем-то приемлемо, но не очень красиво. Костыльный вариант: декодить что-то в районе 1000х1000, а потом ресайзить уже его. Получается лучше. Делал я это для JPEG, остальных распаковывать оказалось сложнее, к примеру, PNG чего только в себе не несет, мне попадались даже 16-бит на канал картинки. В декодерах апи для декодинга части картинок я не встречал, за исключением апи андроида. Но я мало интересовался, проще сделать самому.

На этом пожалуй закончу свое повествование. Что-то еще хотел написать, но уже весь выгорел.

comment:3 Changed 3 years ago by maxim

Спасибо, это очень хорошо.

Note: See TracTickets for help on using tickets.