Ресайз анимированных GIF картинок с помощью PHP + GD

Доброго времени суток.

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

— первый скрипт: попытка два класса GIFDecoder.class.php и GIFEncoder.class.php, которые применялись для генерации GIF анимации, научить ресайзить GIF изображения; в результате конечному пользователю приходится писать чуть ли не третий класс для работы с данными, и судя по отзывам удается только половине; но и это не главное, а главное то что ресайз многих файлов получается некорректным (проблемы с прозрачностью, чередованием, обработкой кадров и тд), этих файлов достаточно много, и поэтому не подходит для поставленной задачи, ссылка на страницу автора

— второй скрипт: класс gifresizer.php — это уже более цельный, специализированный и проще в использовании, и считается лучшим на сегодняшний день в Интернете, но мои эксперименты с ним показали что у этого класса проблемы с обработкой файлов GIF у которых оптимизация графических данных, а также некорректная работа с палитрой, а самое страшное что от некоторых GIF файлов скрипт уходит в бесконечный цикл, о чем правда честно пишет сам автор ссылка на страницу класса

Из перечисленных выше причин ни один класс естественно не подошел. Что же делать?!!! Немного поразмыслив, закатав рукава и отложив на некоторое время основной проект я решил реализовать нереализованное, а именно класс для полноценного и главное корректного ресайза GIF файлов. Рассказывать долго не буду скажу лишь одно, задача была действительно нетривиальной сложности, а все из за того что мудреная структура GIF файла хоть и стандартизованная но стандарт мало кто соблюдает, точнее здесь как стандарт не соответствие стандарту.

В процессе разработки код был переписан три раза (иногда были мысли закинуть это неблагодарное дело), а в результате появился класс для ресайза анимированных GIF файлов с поддержкой прозрачности GIF_eXG. Отличительной особенностью класса является: быстрая, стабильная и корректная работа, а также простота в использовании. Более того класс перебирает (читать: исправляет ошибки) исходный файл дабы тот хоть приблизительно соответствовал стандарту, как результат в ОС Windows в файле где были проблемы с воспроизведением анимации (в данной ОС) после ресайза анимация воспроизводится корректно.

Разбор структуры и анализ каждого бита я выполнял вручную и не прибегал к функциям анализа графического файла библиотеки GD которая в своей природе довольно глюковатая. Она (библиотека GD) подключалась только для ресайза отдельного фрейма. С использованием библиотеки GD для ресайза единичного фрейма связан один глюк (обычно 1 на 50-70 ресайзов) который представляет собой небольшие графические артефакты в виде разбросанных пикселей.

Пример использования класса:

require_once «gif_exg.php»;

$nGif = new GIF_eXG("../image/src.gif",1);
$nGif->resize("../image/dst1.gif",180,180,1,0);
$nGif->resize("../image/dst2.gif",150,150,0,0);

я думаю и так все понятно, лишь небольшие замечания по передаваемым параметрам:
— в конструкторе второй параметр отвечает за оптимизацию структуры, если (1) то выходной файл более компактен по размеру, если (0) то сохранится вся исходная структура;
— в единственной открытой функции resize четвертый параметр указывает соблюдать ли симметрию (1), или не соблюдать (0);
— пятый параметр экспериментальный: (1) попытаться интерполировать пикселы, (0) без интерполяции (рекомендуется);

Место применения класса: создание анимированных аватаров, превю, галерей изображений и тп.

В файле присутствует инструкция на ломаном английском (за нее наперед извиняюсь, проблемы с ин. языками ) для интернационального сообщества, думаю разберетесь.

Ссылки на класс (текущая версия 1.07):

класс GIF_eXG (PhpClasses)
класс GIF_eXG (GitHub)
Поделиться публикацией

Комментарии 42

    +1
    Я не догадался почему не использовали ImageMagick, намекните.
      0
      для анимированных GraphicsMagick лучше
        +1
        ну как же, про это писано переписано: во-первых, ImageMagick не включен в стандартную поставку PHP, а от сюда и дополнительные телодвижения с установкой/настройкой или еще хуже просьбой хостеров поставить даный пакет; во-вторых, не настолько прост и очевиден код ресайза анимированных gif изображений; и в в-третьих, у даного программного продукта также существует проблемы с корректным ресайзом даных файлов; (первая и третья причины основные)
          +1
          Ловко вы избежали телодвижений, угробив пару дней (надеюсь не недель) своего времени.
            +2
            Так интереснее
              0
              когда было решено разработать с нуля рабочий класс, то поставленная цель звучала так «реализовать в скриптовом виде данный функционал не так для себя как для интернет сообщества », если пошел путем использования ImageMagick поверьте было и быстрее и менее сложно
          +10
          Ну и последнее замечание и ссылка на класс: в классе полностью отсутствует форматирование кода, это связано как с оптимизацией размера самого файла так и с принципом «пользователь не должен знать внутреннюю структуру, только открытые функции», а кто хочет модернизировать или дополнить класс без проблем отформатирует так как ему надо.

          Ну и какой смысл от вашего поделия на гитхабе тогда? Если уж выкладываете, то будьте готовы к оцениванию вашего кода другими разработчиками, но с таким форматированием это отбивает всякое желание.
          Пользователь будет смотреть на картинку, а не копаться в коде. Ваши бессмысленные оптимизации размера файла мне непонятны. Экономия на спичках.
            0
            в принципе резонное замечание, но в данном случае GitHub использовался не как «средство совместной разработки», а как «средство публикации»
              0
              В том то и дело, «публикации» от слова «публика», а такой код нельзя показывать публике.
            +5
            в классе полностью отсутствует форматирование кода, это связано как с оптимизацией размера самого файла так и с принципом «пользователь не должен знать внутреннюю структуру, только открытые функции»

            Место на диске экономите? Выкладывать такое на гитхаб, простите, мудатсвто :)
              +2
              Нет, просто автор хотел незаметнее авторство своё увековечить.
              $con = "\x21\xFE\x0Eyuriy_khomenko\x00" . $con;
              

              Ну правда, отформатируйте код, тем более в IDE выдаются ошибки, типа Undefined variable ($buffer_add, $lc_mod, $lc_palet, $head, $gr_mod, $palet, $f_buf, $con).
                –1
                Нет, просто автор хотел незаметнее авторство своё увековечить.))))) увековечить да, скрыть нет, но в шапке описания также автор присутствует не пойму разве у меня нет прав сделать это? IDE выдаются ошибки, типа Undefined variable наверное это не ошибки, а предупреждения «хорошего стиля программирования», переменные определяются и инициализируются только там где они применяются не более и не менее, и не нужно к коду PHP применять параноидальные требования аля С++
                  +1
                  Так с отсутствия «хорошего стиля программирования» и начинаются:
                  — Ну а че? Пусть себе лежат нигде не используемые приватные функции, тебе что мешают?
                  — Зачем включать параноидальные E_NOTICE, подумаешь, необъявленная параменная проскочит.
                  — Ну и что, что WARNING'и сыпятся, код-то работает!
                  — Что-то error.log файлы распухают, давай их выключим, кто в них смотрит?
                    0
                    еще можно покритиковать сам PHP за слабую типизацию переменных и тд и тп, скрипт написан не выходя из тех рамок которые определены средой разработки то есть самим PHP, NeLexa ошибся указывая на ошибку типа Undefined variable (переменная не определена), здесь присутствует место «не инициализация переменной при объявлении», а на это решено было пойти намеренно через оптимизацию, сейчас объясню почему: переменные перечисленные NeLexa (и не только они) объявляются до входа в область видимости цикла для доступности к ним после выхода из него (хотя PHP позволяет и доступ к лок переменной после выхода из лок области, все вопросы к разработчикам PHP, я как раз тут и придерживался общепринятого стиля), почему же я ее сразу не инициализировал? а все потому что сразу при входе в лок область цикла переменной присваивается значение, существуют GIF файлы состоящие с 500 и даже более фреймов (количество не ограничено), для каждый фрейм прогоняется несколько раз через циклы для разбора, и это не такая быстрая задача скорее наоборот, а теперь давайте посчитаем 500 * 1(в лучшем случае, здесь могло быть и 2 и 3) * 10 (количество переменных, их больше десять просто для наглядности) = 5000 раз бессмысленной инициализации(и это в одном месте) за которой идет присваивание, когда и так не факт что скрипт будет выполнятся дефолтное для PHP скрипта время (30 сек); 500 фреймов может и редкость, но 100 обыденное дело и тут уже борьба за скорость ресайза (боролся за каждую долю секунды, для конечного пользователя), а теперь представте что пользователь ресайзит сразу пачку файлов, я в своем проекте масштабирую сразу 3 картинки (один исходный файл в 3 вариантах размера), в скрипте кстати можно увидеть очень длинные выражения и все из за того чтобы не использовать промежуточные переменные… думаю приблизительно объяснил причину
              –2
              ссылку на гитхаб убрал
                0
                какой то не конструктивное обсуждение, «зачем», «почему», «гитхаб», «форматрование»… жду поста типа «нашел файл с проблемой ресайза»
                  +1
                  сейчас на PhpClasses скину отформатированную версию со всеми правилами
                    +1
                    Лучше бы на GitHub, там лучше проекты развивать. Или вы сразу и туда, и сюда?
                      0
                      хорошо, и на гите обновлю, но только завтра после работы
                    0
                    вернул ссылку на GitHub с полным форматированием кода
                      0
                      Шикарный вывод ошибок. :)
                              if ($this->er) {
                                  printf("ERROR: signature file is incorrectly");
                                  return 0;
                              }if ($new_x == 0 || $new_y == 0) {
                                  printf("ERROR: size height or width can not be equal to zero");
                                  return 0;
                              }$des = Array(0, 0, 0);
                      
                        0
                        Ладно вам =)
                        Позиция автора ясна — сделать так чтобы работало быстро.
                        Если это причесать, убрать странности типа:
                        $lc_i = ord($this->gif[$this->pnt + 2]);
                        $sum = 2;
                         while (($lc_i = ord($this->gif[$this->pnt + $sum])) != 0x00) {
                           $sum+=$lc_i + 1;
                        }
                        

                        Получится вполне ничего ;)
                          0
                          укажите на странность, чтобы было что объяснять, как я писал выше код три раза переписывался (практически изменился до неузнаваемости), сейчас в коде практически все вылизано и заточено под немогу, первый вариант скрипта «ложился» через нехватку памяти, для файла размером 6 Мб требовалось памяти 700 МБ (это была первая проблема), вторая как я писал скорость самого ресайза, которую тоже пришлось решать и вроде получилось, вами приведенный код наверное странноват (сам правда не пойму причину странности) из за оптимизации ресайза, уточните непонятный кусок кода все объясню или весь приведенный код объяснять?
                            0
                            Ну хотя бы зачем первое присваивание
                            $lc_i = ord($this->gif[$this->pnt + 2]);
                            Раз уж вы ратуете за экономию тактов процессора?
                              0
                              в первом варианте скрипта я циклом проходился по всем битам файла во время анализа и рассортировки всей структуры по полочкам, но после когда код был реализован были большие проблемы с производительностью особенно на больших файлах, в последующем я изменил тактику, теперь я не бегу по всем битам файла, а откусываю кусками, в данном случае идет разбор расширения (в формате файла GIF89a их предусмотрено 4), а именно расширения-коментаррия, стандарт дает гарантию (и его обычно придерживаются), что в третьем бите хранится число обозначающее размер данных мы его сразу и получаем, то есть если в этом поле будет сохранена книга Война и мир (что вполне реально), мы ее откусим одним куском без пробега по ее символам, дальше стандарт можно сказать ничего не гарантирует (точнее его никто не соблюдает) то приходится просчитывать размер всего расширения циклом(но обычно основной объем данных ЗНАЧИТЕЛЬНО превышает размер технических данных отсюда и выигрыш), и все суммируя, после уже суму которая набежала и которая равна размеру расширения, передаем в функцию которая непосредственно выдирает эти данные из файла и сохраняет в переменную класса… думаю ясно объяснил
                                0
                                Понятно, но бесполезно.
                                Я намекал на то что эту строку и еще пару аналогичных можно выкинуть, т.к. они не несут никакой практической пользы. При входе в цикл $lc_i в любом случае инициализируется значением ord($this->gif[$this->pnt + $sum]), где $sum = 2.
                                  0
                                  я только что присмотрелся, да вы полностью правы, это торчат уши с второй версии скрипта, забыл убрать, убрал только в 88 строчке скрипта, а во всем проекте забыл, спасибо что ткнули носом, если вы не против, могу отблагодарить вас в шапке следующей версии скрипта как человека помогающего развитию кода…
                                    0
                                    Просто посмотрите в сторону NetBeans или phpStorm. В последнем помимо стандартного парсера кода, показывающего простейшие детские косяки есть возможность прикрутить phpMess Detector и CodeSniffer.
                                      0
                                      спасибо обязательно посмотрю, NetBeans стоит в системе но по старинке пишу в Notepad++, надо постепенно мигрировать на те средства что вы предложили или в крайнем случае привлекать эпизодично к работе
                          0
                          Шикарный вывод ошибок. :)и что, в чем собственно проблема.......?
                            0
                            Как их отлавливать? Через ob_start()?
                          0
                          кого «их»? причем здесь ob_start? пишите яснее чтобы я не догадывался… скрипт отлавливает всего два простых вида ошибок: первая — неправильные параметры, которые отвечают за новый размер изображения(размер не может быть нулевой); вторая — неправильный формат файла, или другими словами все не gif файлы отметаются;
                            0
                            Я таки поясню =)
                            Обычно при возникновении ошибки функция должна вернуть false/NULL/кинуть исключение, чтобы на уровне выше можно было адекватно отреагировать на неё. В вашем случае отдается 0, что можно понять и… в stdout срется текст, что не есть гуд.
                            Почему? Если ваш скрипт будет использован на фронтенде, а не как консольная приблуда — чтобы гарантировать нормальный вывод данных пользователю необходимо использовать ob_start чтобы заглушить вывод сообщения.
                              –1
                              да в принципе правильное замечание, сперва хотел прикрутить выброс исключения, но потом ради двух ошибок плюнул на это дело, думал также возвращать ошибку в виде картинки, или массив в первом элементе код ошибки во втором описание, но так у меня в коде где используется данный класс параноидальная проверка всего входящего то от этих идей отказался, в принципе не только у меня и а каждого должна быть может не такая но все же достаточная фильтрация дабы некорректные данные в сам класс не дошли (вылов подобных ошибок в классе мягко говоря не верно), здесь она(обработка ошибок) скорее для отладки, и последнее как я писал в паспорте скрипта скрипт разрешается изменять/модифицировать, пусть пользователи реализуют такую обработку которая им надо
                            0
                            с подачи AotD вышел профиксеный вариант класса GIF_eXG (текущая версия 1.03)
                              0
                              обновил версию класса GIF_eXG (добавлена поддержка некоторых нестандартных форматов файлов) (текущая версия 1.04)
                                0
                                Вот такое масло масляное
                                 $lc_i = ord($this->gif[$this->pnt + 9]) & 128 ? 1 : 0; 
                                ...
                                if ($lc_i) { ...
                                
                                
                                

                                раз уж мы говорим об оптимизации стоит убрать, ибо оно ничем не оправдано.

                                А здесь я бы на вашем месте использовал бы case. Конечно в данном примере это не даст большого выигрыша, но привычка плохая… Ибо на месте массива, может оказаться ёмкий по времени метод, который будет вызываться несколько раз вместо одного.
                                ...
                                 elseif ($this->gif[$this->pnt + 1] == "\xFF") {
                                ...
                                 } elseif ($this->gif[$this->pnt + 1] == "\x01") {
                                ...
                                
                                /**
                                А можно так
                                */
                                
                                switch ( $this->gif[$this->pnt + 1] ) {
                                    case "\xFF":
                                    ...
                                    case  "\x01":
                                    ... 
                                }
                                 


                                А вообще, раз уж вы представляте код комьюнити, стоит оформить его, как для комьюнити. Я конечно не говорю о композере, но пхпдоки и человечное форматирование кода, были бы очень кстати, тем более в любой современной IDE, это делается нажатием одного хоткея ;)
                                  0
                                  Спасибо за комментарий. Сперва спрошу, вы сами анализировали код или с помощью анализаторов? Мне выше посоветовали phpMess Detector но как я его не прикручивал так и не прикрутил. Хочу также заметить что никак не ожидал такой дотошной разборки кода, но никак не критикую, а наоборот рад за это и благодарю каждого кто приложил/приложит усилия для улучшения кода (и каждого обязательно отмечу в следующем релизе класса). А теперь все по порядку:
                                  1) масло масленое
                                   $lc_i = ord($this->gif[$this->pnt + 9]) & 128 ? 1 : 0; 
                                  ...
                                  if ($lc_i) { ...
                                  

                                  вы это вырвали с контекста
                                  $lc_i = ord($this->gif[$this->pnt + 9]) & 128 ? 1 : 0;
                                  $head = $this->gtb(10);
                                   if ($lc_i) {
                                  

                                  как раз вторая строчка передвигает глобальный указатель, и нам надо успеть проанализировать второй байт(точнее бит) от текущего места, дабы знать куда двигаться после того как в переменную $head будет записан один из блоков GIF файла, в данном случае проверяем присутствие локальной палитры, можете сделать тест, отформатировать так как вы говорите, прогнать через класс анимированный GIF файл, а потом еще раз прогнать отресайзеный результат (как раз в выходном файле появляется локальная палитра) и как раз тут скрипт войдет в крутое пике или ступор (как кому нравится), поэтому здесь такая последовательность операторов;
                                  2) второе замечание принимается, почему то ну недолюбливаю конструкцию switch-case, обычно использую если вариантов выбора более пяти, а класс по вашему совету обязательно откорректирую;
                                  3) третье замечание в принципе тоже верно, но из за неимения свободного времени плюс к этому прабл с ин языками (а комьюнити у нас не только русскоязычное) пришлось ограничится только шапкой, не понял причем здесь компосер, насколько я помню это средство работы с библиотеками, а я использую только одну стандартную библиотеку GD, и последнее, а чем вам форматирование собственно не нравится? это как раз и есть NetBeans, или может вы предпочитаете аля PhpStorm ))))<source
                                    0
                                    опечатка, в коде выше мы анализируем не второй байт а десятый (что в принципе ясно с кода)…
                                      0
                                      Ну насчёт первого — я до сих пор не очень понял, почему там так, но видимо там действительно так нужно.

                                      Анализаторы использую редко и ИМХО — это плохая практика. Код ревью может быть так же эффективно и при этом приносит пользу всем участникам :)
                                    0
                                    вышла новая версия класса — 1.05, включающая общие кодовые улучшения, а также поддержка новых нестандартных форматов GIF файлов
                                      0
                                      версия 1.07 — корректный ресемплинг

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

                                      Самое читаемое