Пользователь
22 октября 2012 в 18:53

Дизайн → Комплексная подготовка сайта к Retina из песочницы

К написанию этого материала меня побудила статья и назревшая необходимость адаптации нескольких своих сайтов к поддержке Retina-дисплеев. Под адаптацией я понимаю подготовку изображений высокого разрешения на страницах сайта.

Пожалуй лучшим способом адаптации на сегодняшний день является способ c background-image в CSS. Но он сложно применим к обычным изображениям в теге . Поэтому я решил определить для себя список необходимых мер по достижению результата в комплексе и продолжить поиски решения. Ниже описано два способа, каждый из которых применим к своим задачам. Показанные решения не претендуют на открытие, скорее это агрегация существующих способов, описанных ранее моими коллегами по цеху веб-разработок и небольшие собственные дополнения.

Полумера или простое решение
Решение для Apache
Решение для nginx

Полумера: обработчик background-image


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

.elem {
    background-image: url(elem.png);
}

@media only screen and (-webkit-min-device-pixel-ratio: 1.5),
    only screen and (min-resolution: 144dpi) {
        .icon {
            background-image: url(elem@2x.png);
            }
    }

Разумеется, класс elem приведен для примера и нужно адаптировать этот css под ваши нужды. Ну и не забываем про подготовку дополнительного комплекта нарезки изображений дизайна сайта. Разумеется, тут придется поработать, особенно если дизайн сайта насыщен изображениями, но я сторонник разового выполнения таких работ, особенно если их сравнивать с разнообразными «костылями».

Постановка задачи комплексной поддержки Retina


Для себя определил следующие критерии для решения задачи комплексного перевода сайта на поддержку Retina:
  1. способ должен быть универсальным и должен включать обработку всех без исключений изображений на сайте;
  2. способ не должен приводить к обновлению страниц в результате какого-либо редиректа;
  3. способ должен быть максимально лояльным к пользователю, он не должен закачивать в браузер пользователя исходную картинку и подготовленную для Retina.

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

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

Механизм загрузки и обработки графических файлов описывать не буду, поскольку он учитывает индивидуальные особенности проекта, но суть сводится к тому, что из загруженной пользователем картинки необходимо сформировать рабочее изображение для сайта и его копию, оптимизированную для Retina, в том случае, если качество загруженного изображения это позволяет. Например, в рассматриваемом проекте максимальная ширина контентных изображений составляет 800px. Разумеется, если пользователь загружает изображение с шириной 400px, сформируется только его копия шириной 400px. А вот если пользователь загрузит изображение с шириной хотя бы 1600px, что случается довольно часто, мы сможем порадовать посетителей сайта с Retina.

При этом нужно учесть, что загруженные ранее изображения сохранить в высоком разрешении уже не получится, а уменьшать значения height и width без ведома пользователя как-то неправильно. Поэтому способ должен предусматривать работу как с существующими @2x картинками, так и без оных.

Решение для Apache


Заранее обозначу, что решение далеко от идеального, оно слишком затратно по ресурсом сервера и не рекомендуется к применению. Но, к сожалению, в случаях отсутствующего nginx или доступа к его конфигу, необходима альтернатива и она описана ниже.

Первым шагом в шаблон страниц (ну или в один из ваших подключаемых js файлов) добавляем простой js-скрипт (важно чтобы этот код выполнился до начала загрузки контента страницы и любого CSS, в котором указаны изображения, которые необходимо подменить):

<script>document.cookie='resolution='+Math.max(screen.width,screen.height)+("devicePixelRatio" in window ? ","+devicePixelRatio : ",1")+'; path=/';</script>

JS код сформирует куку, по которой серверный скрипт может понять с каким дисплеем пришел на сайт пользователь. Следующим шагом во многих описанных способах формируется редирект посредством js и скрипты CMS, анализируя куку resolution, выдают правильную версию графики. Но мы не ищем легких путей и редирект не применяем. Посредством .htaccess или конфига apache устанавливаем обработчик куки вне скриптов CMS сайта:

RewriteCond %{REQUEST_URI} ^/images
#RewriteCond %{REQUEST_URI} !^/images/excluded
RewriteCond %{HTTP_COOKIE} resolution=([^;,]+),2
RewriteCond %{REQUEST_URI} !@2x\.(jpe?g|gif|png)$
RewriteRule ([^.]+)\.(jpe?g|gif|png)$ $1@2x.$2 [L]

Первая строка RewriteCond подсказывает скрипту в какой папке лежат обрабатываемые файлы изображений.
Вторая (закомментированная) строка позволяет добавить папку-исключение в обработчик.
В третьей строке RewriteCond мы избавляемся от того самомго редиректа, определяя куку, которая была выставлена при загрузке страницы.
Четвертое условие исключает из обработки уже отданные изображения высокого разрешения (с фрагментом " @2x" в конце имени файла).
В пятой строке мы определяем к изображениям какого формата необходимо применить подмену.

Преимущества описанного способа:
  • решение работает для всех изображений сайта;
  • нет редиректа страницы;
  • при отсутствии куки «resolution» пользователь увидит дефолтный контент страницы.

Недостатки:
  • (по мнению автора этого способа) не учтен пользовательский способ подключению к интернету и возможность выкачивания им изображений в высоком разрешении;
  • существенный недостаток, с моей точки зрения, в необходимости использования редиректа на hi-res изображение посредством Apache;
  • также, с моей точки зрения, существенная проблема состоит в отсутствие проверки наличия @2x изображения на сервере.

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

В .htaccess строка
RewriteRule ([^.]+)\.(jpe?g|gif|png)$ $1@2x.$2 [L]

заменена на
RewriteRule ([^.]+)\.(jpe?g|gif|png)$ retina.php?img=$1@2x.$2

Простой php скрипт определяет наличие запрашиваемого изображения на сервере и выдает его. Если файл изображения не найден, скрипт отдает обычную (не hi-res) картинку. Разумеется с проверкой запрашиваемых данных:

<?php
if (!empty($_GET['img']) && mb_substr_count($_GET['img'], '@2x'))
    {
    function isImg ($dress)
        {
        if (function_exists('exif_imagetype'))
            $type_file = exif_imagetype ($dress);
        else
            {
            $type_file = getimagesize ($dress);
            $type_file = $type_file[2];
            }
        // проверка типа файла изображения
        // 1 = GIF, 2 = JPG, 3 = PNG
        if (in_array($type_file, array(1,2,3)))
            {
            if (type_file==1)
                header("Content-Type: image/gif");
            elseif ($type_file==2)
                header("Content-Type: image/jpeg");
            else
                header("Content-Type: image/x-png");
            header(sprintf('Content-Length: %d', filesize($dress)));            
            return true;
            }
        unset ($type_file);
        return false;
        }

    $_GET['img'] = $_SERVER['DOCUMENT_ROOT'].'/'.$_GET['img'];
    
    if (file_exists($_GET['img']) && isImg ($_GET['img']))
        die (file_get_contents($_GET['img']));
    else
        {
        $_GET['img'] = str_replace("@2x", "", $_GET['img']);
        if (file_exists($_GET['img']) && isImg ($_GET['img']))
            die (file_get_contents($_GET['img'])); // я сознательно не использовал readfile(), т.к. хостеры часто ее блокируют. Правильно отдаем файл: http://habrahabr.ru/post/151795/
        }
    header("HTTP/1.1 404 Not Found"); // если нет картинок
    }
else
    header("HTTP/1.1 403 Forbidden");
?>

Разумеется, это изменение увеличило нагрузку на сервер, но другого решения я не видел.

Решение для nginx


Благодаря подсказке docomo и progit появилась гораздо более изящная и верная, на мой взгляд, версия решения задачи. Если на вашем сервере есть nginx и есть доступ к его конфигу, можно избавиться от неоправданно высокой нагрузки которую даст php скрипт, а за одно и сам Apache, внеся не сложную корректировку.

На момент описания этого способа я немного изменил и js для шаблонов сайта.

Итак, новый js:
<script>if ("devicePixelRatio" in window && window.devicePixelRatio > 1) document.cookie='hires=1; path=/';</script>

Таким образом cookie hires выставляется только в том случае, если нам нужно формировать комплект изображений «высокого разрешения».

Изящество описанного выше способа сохраняется (на любом сайте без переделки кода скриптов подмена может быть осуществлена и для background css и для тега ). А вот недостаток способа с php скриптом обходится настройкой конфига nginx:

if ($http_cookie ~ "hires" ) {
    set     $hires 1;
}
location ~* ^(.+)@2x.(jpg|jpeg|gif|png)$ {
    try_files       $uri $1.$2 =404;
}
location ~* ^.+.(jpg|jpeg|gif|png)$ {
    if ($hires = 1) {
        rewrite ^(.+).(jpg|jpeg|gif|png)$ $1@2x.$2;
    }
}

Таким образом, nginx берет на себя всю работу со статикой.

И да, спасибо progit за немаловажное уточнение: При таком подходе для всех картинок на сайте обязательно указывать width и height. Для через аттрибуты или через css-свойства, а для фоновых картинок нужно указывать background-size: width height.

За основу изложенного способа решения задачи взят метод Automatically serve retina artwork by Sam Sehnert.

UP: Всем Хабраюзерам спасибо, внес правки по комментариям.
Игорь @izelenyuk
карма
15,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Спецпроект

Самое читаемое Дизайн

Комментарии (39)

  • 0
    Как вариант:
    Узнаем jsом ppi и если у пользователя retina дописываем в href всех img «2х» перед расширением.
    • +2
      Если речь об обработке загруженной страницы, то пользователь накажет себя на загрузку неоптимизированных и оптимизированных изображений (т.е. оба комплекта).
      • +1
        Будет эдакий progressive loading.
        Сначала отображается упрощенная версия, потом подгружается более красивая, в которой в 4 раза больше пикселей.
        Если имеем gprs с ограниченной скоростью и неограниченным трафиком то это самое оно.
  • +4
    Ох, нагородите сейчас, лучше начните использовать css4 image-set, с префиксом оно уже поддерживается в Safari 6 and Chrome 21, для остальных же high dpi устройств и избранных изображений используйте вдвое больше пикселей втиснутых в нужных объём и более агрессивное сжатие, всё

    blog.cloudfour.com/safari-6-and-chrome-21-add-image-set-to-support-retina-images/
    • 0
      А как быть с пользовательским контентом?
      • 0
        А это должно быть на стороне CMS при формировании страницы. У вас же кука есть.
        И раз вы можете заставить CMS делать две версии картинки, заставьте её отдавать разные линки в /img/.
        • 0
          А если усложним задачу кэшированием?
          • 0
            А так сложно закешировать две версии? =)
            Я вообще не знаю, я только на Django пишу, и не вижу никаких препятствий.
            В других местах могут быть.
            • 0
              Подумал, в принципе выполнимо. Динамический контент перед кэшированием обработать по выставленной куке и сформировать два кэша не проблема. Но ведь есть еще и статические шаблоны. Как быть с ними? Тоже два комплекта? ИМХО оно получится слишком трудно-управляемым со временем.
              • 0
                В Django я б написал шаблонный тег IMG и везде применял бы его.
                А для передачи куки в шаблон сделал бы Context Processor, так что тег бы всегда знал что в куке.
                • 0
                  Хм, но <img> это только часть проблемы. Во-первых есть CSS. Во-вторых, есть различные ajax загрузчики контента на сервер, появляется необходимость обработки статических шаблонов и т.д. и т.п. Другими словами, мы либо не покрываем всех картинок либо способ становится неуправляемо интегрируемым со скриптами CMS (что также не очень хорошо, так как лишает гибкости).
                  В моем случае, при необходимости, в течение пары минут статика перетаскивается на nginx без перекройки всего кода. Если бы удалось уйти от серверного php скрипта только с помощью обработчика в nginx способ стал бы реально универсальным. Есть спецы? Ау?
                  • +1
                    Я сделал вот таким образом:

                    if ($cookie_retina = 1) {
                        rewrite ^/images/(.*?)\.(jpg|png|gif) /images/$1@2x.$2;
                    }
                    location ~ /images/(.*?)@2x\.(jpg|png|gif) {
                        try_files $uri /images/$1.$2 =404;
                    }
                    

                    Если запрашивается картинка @2x и ее нет на сервере, то отдается обычная картинка.
                    Вместо =404 можно написать свой обработчик (например, переслать в php-fpm и тд)

                    Название куки, которую нужно установить «retina». Соответственно, 1 — ретина, что-то другое — не ретина.

                    При таком подходе для всех картинок на сайте обязательно указывать width и height. Для через аттрибуты или через css-свойства, а для фоновых картинок нужно указывать background-size: width height.
                    • 0
                      Странно, что никто не «отметил» это решение. В разы оптимальней по нагрузке, чем описанное автором топика (для Арача).
                      • 0
                        Со вчерашнего вечера «отметили» это решение и обсуждали, внес правки в пост.
    • –1
      A на Android планшетах с ретиной это сработает?
      • 0
        Не пробовал, нужно лишь выполнить JS и посмотреть что вернет. Думаю да.
        • 0
          JS то ни куда не денется.
          Я про css4 image-set.
          • 0
            Технический приём, связанный с употреблением CSS4 image-set, не может работать почти ни на одной из современных версий системы Android, раз уж о его появлении нам сообщили в августе 2012 года.

            Есть ли хоть одна версия Android, появившаяся после августа?

            Только одна — 4.1.2, за октябрь 2012 года. (Предыдущая версия, 4.1.1, появилась 9 июля.)

            Много ли у кого версия 4.1.2 установлена? (Вопрос риторический, с подразумеваемым отрицательным ответом.)

            Сопровождался ли её выход изменениями в коде Android Browser, или производители решили «работает — не трогай!»? (Вопрос не риторический, ответа я не знаю.)
            • 0
              Логично, спасибо.
              Там еще Chrome есть, теоретически там может сработать.
  • +1
    if (file_exists($_GET['img']) && isImg ($_GET['img']))
            die (file_get_contents($_GET['img']));
    

    Оригинальный способ отдачи контента. Лучше использовать функцию readfile(), которая не читает в память весь файл перед выводом. А еще лучше сказать веб-серверу, какой файл нужно отдать с помошью специального заголовка. Но такой фокус я знаю только для nginx, не знаю, есть ли в apache что-то подобное.
    • 0
      Тут согласен. Но, readfile() не везде доступна.
      А вот по поводу заголовка для nginx можно по-подробнее?
    • 0
      В этом топике для Апача описывали способ.
  • 0
    > Разумеется, это изменение увеличило нагрузку на сервер

    Разумеется.
    Если я вас правильно понял, то убрать весь этот олдскульный огород с прогоном каждой картинки через php+apache вам помогут три строчки на nginx.
    Ставите nginx. Всем изображениям в коде сайта добавляете префикс "/2x", если человек с ретины.
    Скажем, было /images/pic.jpg, стало /2x/images/pic.jpg
    Далее пишете в конфиге:

    location ~ ^/2x/(.+)$ {
        try_files       $uri /$1 =404;
    }
    


    Оптимизированные изображения, соответственно, заливаете в папку /2x соответственно текущей структуре.
    • +3
      Перечитал еще раз задачу. Решение для кук:

      1. Аналогично ставите куку перед загрузкой страницы: если ретина, кука retina = 1, если нет, то куки retina нет.
      2. Пути к изображениям остаются неизменными, никаких префиксов /2x и т.д.
      3. Кладете оптимизированные изображения в папку /2x/… и далее согласно текущей структуре файлов на сайте.
      4. Конфиг nginx:

                      if ($http_cookie ~ "retina" ) {
                              set     $retina 1;
                      }
                      location ~ ^/2x/(.+)$ {
                              try_files       $uri /$1 =404;
                      }
                      location ~* ^.+.(jpg|jpeg|gif|png)$ {
                              if ($retina = 1) {
                                      rewrite (.+)    /2x$1;
                              }
                      }
      


      Суть в том, что сервер проверяет наличие куки «retina». Если она есть, перенаправляет файлы на префикс /2x. Далее смотрит: если файл есть в 2x — отдает ретина-версию, если нет — отдает файл по старому пути.
      • 0
        docomo, спасибо, так действительно можно создать универсальный метод. Я так понимаю, описать в конфиге nginx правило работы с постфиксом в имени файла невозможно?
        • 0
          Ну почему же, возможно. Вот для постфикса @2x:

                          if ($http_cookie ~ "retina" ) {
                                  set     $retina 1;
                          }
                          location ~* ^(.+)@2x.(jpg|jpeg|gif|png)$ {
                                  try_files       $uri $1.$2 =404;
                          }
                          location ~* ^.+.(jpg|jpeg|gif|png)$ {
                                  if ($retina = 1) {
                                          rewrite ^(.+).(jpg|jpeg|gif|png)$ $1@2x.$2;
                                  }
                          }
          


          В этом случае файлы остаются там же, где и были, просто pic.jpg становится pic@2x.jpg (описанный в вашем посте вариант). По-прежнему проверяем на куку «retina», если есть — отдаем @2x изображения. Пути к картинкам править, опять же, нигде не надо.
          • 0
            Великолепно! Вечером попробую, если все получится, внесу правку в статью для глупого решения с apache и отличного варианта с nginx. Спасибо. Плюсанул бы, но кармы не хватает :)
            • 0
              Apache в принципе-то не виноват. Просто в вашем варианте картинки гоняются через php-скрипт, а это большая нагрузка. mod_rewrite апача я уже не помню за давностью, но уверен, что на нем тоже можно реализовать подобную схему проверки существования файла.
              • 0
                Ну под глупым решением с Apache я и имел ввиду php-скрипт. Но и статику лучше конечно-же отдавать nginx, поэтому ваш способ сейчас в явном выигрыше.
          • 0
            Странно, в конфиге nginx этот код приводит к тому, что картинки вообще не отображаются. Куку не выставлял, опробовал без нее.
            • 0
              Проверьте параметр root в конфиге хоста.
              Чтобы тут не разводить треды, кинул вам в личку полный конфиг хоста, например.
      • 0
        Собственно выше это решение было предложено и оно (вместе с вашим) наиболее оптимальное для данной задачи.
  • 0
    лучшим способом адаптации на сегодняшний день является способ c background-image в CSS. Но он не применим к обычным изображениям в теге <img>
    Вообще-то он вполне применим.

    В вики Stylish излагается вот какой действенный рецепт подмены изображения при помощи CSS:

    IMG#идентификаторКартинки {
       /* обнуляем размеры имеющейся картинки: */
       height: 0 !important;
       width: 0 !important;
       /* задаём размеры новой картинки: */
       padding-left: 125px !important;
       padding-top: 25px !important;
       /* задаём адрес новой картинки: */
       background: url(http://example.com/адрес/иллюстрации) no-repeat !important;
    }
    

    Достаточно засунуть этот код внутрь Вашей обёртки @media only screen and (-webkit-min-device-pixel-ratio: 1.5), only screen and (min-resolution: 144dpi)») — и дело сделано.
    • 0
      Mithgolда, действительно, это вариант, но:
      1. В большом проекте мы будем вынуждены картинки background дизайна в css обрабатывать вручную;
      2. Способ не подойдет при большом количестве «внедрений» картинок (во всех шаблонах надо указывать id картинки, а если есть генерация лайтбоксов на js — опять же придется перерыть весь код и в части мест внести изменения не удастся, т.к. id тега может быть занятым;
      3. Самое главное. Для каждой страницы @media css придется генерировать скриптами. А если на страницу аяксом подгрузится еще картинка — всплывут проблемы.

      За способ спасибо, для статики он применим на ура!
  • 0
    А что, сейчас все экраны с высокой плотностью Ретиной называют?
    • 0
      только макаводы ;) ведройды называют hdpi и xhdpi
  • 0
    С одной стороны, куки для того и придуманы, чтобы влиять на генерацию контента, но с другой, содержимое «статического» файла будет определяться не только урлом, но и кукой — костыльно выглядит, как минимум всякое кеширование ломается сходу, и далеко не все cdn для статики смогут такие запросы правильно обработать.

    Хорошее решение должно оперировать только урлами. Это сложно, не спорю, надо лезть в движок сайта, одними шаблонами с добавленным JS кодом не обойдешься уже… ((

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.