googlego
Сжатие инструкций для CFR[]
Выложил свою программу для перевода растра в инструкции для интерпретатора CFR[]. Написана, к слову, на «Гоу».
Самая интересная часть там — про оптимизацию.
Я реализовал довольно очевидную идею — делаем две переменные, одна указывает на начало инструкций, вторая — на середину. Вырезаем подстроку между двумя этими индексами, ищем её сразу за правым индексом, если находим, заменяем на вырезанное, оборачивая в цикл и начинаем сканировать заново. Если не находим, сдвигаем сначала правый указатель к началу, повторяем поиск, потом левый к центру, повторяем поиск. Естественно по ходу надо отбрасывать куски, где не сбалансированы скобки, это совсем просто.
Кроме того, у нас есть восемь вариантов обхода картинки — по двум направлениям из каждого угла, что, если нет симметрии, даёт восемь разных наборов инструкций, которые сжимаются
При этом немного смещается координаты отрисовки картинки. Её можно было бы возвращать обратно, но зачем? Интерпретатор и так рисует в центре холста, так что сдвиг — это не принципиально, только размер кодирующей последовательности увеличится.
Статический анализ в Go
С интересом попробовал в действии gocritic — статический анализатор для Гоу, статья о котором в прошлом месяце появилась на «Хабре». Он в самом начале пути,
Для теста взял один из внутренних проектов нашей компании — узкоспециализированный поисковый сервис, который размещает данные в памяти. Проекту четыре года, можно было ожидать, что в нём скопилось
Просто так gocritic не запустился, упал
После успешного запуска получилось примерно два экрана замечаний, большей частью относящихся к устаревшим за время существования проекта практикам, но парочка замечаний была действительно полезна. Жаль, что пока нет интеграции
Как бы то ни было, большинство замечаний были однотипными и много времени правка не заняла — помогли регулярные выражения. Теперь, даже со включенными экспериментальными и опциональными проверками, анализатор ругается только на одно место — он ошибочно считает, что раз у функций одинаковая сигнатура, то и сами функции одинаковые, рекомендует дать им имя и вызывать по имени.
Как я уже говорил,
rangeExprCopy: copy of utf (256 bytes) can be avoided with &utfstdExpr: can replace "POST" with net/http.MethodPostПо моему мнению очень полезный проект, желаю ему дальнейшего развития!
Как быстрее измерить длину строки в Go?
Интересная штука, с которой сталкиваются начинающие разработчики в Гоу — как измерить длину строки в этом языке. То есть речь не идёт даже о быстроте, как, чёрт возьми, вообще это сделать?
Встроенная функция len возвращает длину в байтах, именно поэтому англоязычные разработчики часто и не знают, что проблема есть — строки латиницы, что в байтах, что в символах (в Гоу они называются «руны») занимают одинаково.
Я знаю два способа измерить длину строки:
package main
import ("io/ioutil"; "unicode/utf8")
func main() {
bytes, _ := ioutil.ReadFile("test.txt")
// считаем сколько рун встречается в строке
print(utf8.RuneCountInString(string(bytes)))
print("\n")
// считаем длину массива рун
print(len([]rune(string(bytes))))
}По работе понадобилось измерить длину строки быстро — у меня здоровый цикл, где она постоянно измеряется, оказалось, что в тысяче итераций на тексте книги «На всех пара́х» Терри Пратчетта разница в две секунды — выигрывает преобразование в массив «рун».
Нужен быстрый способ — используйте len([]rune(…)).
Go и Oracle
На работе произошло

Проблемы начались, когда я попытался подключиться из прототипа к СУБД «Оракл» (такое было условие задачи). Сначала я, как и наверное многие до меня, нашёл драйвер
Для начала, проблема возникла с кодировкой. У нас в базе используется кодовая таблица 1251. Сначала (тут я сам дурак) мне показалось, что драйвер не учитывает кодировку из переменных откружения (проблема оказалась
charset := C.CString(charset_dsn)
defer C.free(unsafe.Pointer(charset))
env := new(C.OCIEnv)
rv = C.OCIEnvNlsCreate(
(**C.OCIEnv)(unsafe.Pointer(&env)),
C.OCI_DEFAULT | C.OCI_OBJECT,
nil, nil, nil, nil, 0, nil, 0, 0,
)
charset_id := C.OCINlsCharSetNameToId(unsafe.Pointer(env), (*C.oratext)(unsafe.Pointer(charset)))
rv = C.OCIEnvNlsCreate(
(**C.OCIEnv)(unsafe.Pointer(&conn.env)),
C.OCI_DEFAULT | C.OCI_OBJECT | C.OCI_THREADED | C.OCI_NO_MUTEX,
nil, nil, nil, nil, 0, nil,
charset_id, charset_id,
)Этот код должен работать вместо OCIEnvInit, если нужно использовать кодировку. Но, повторюсь, это проблема не драйвера, просто этот тот случай, когда «ложечки нашлись, а осадок остался».
Потом оказалось, что в драйвере нет превыборки (prefetch),
Я подумал, что постоянно решать проблемы драйвера — не мой путь и нашёл прекрасный Goracle, чуть более сложный, но обладающий всеми мыслимыми возможностями, порт с пайтонячего драйвера, написанного на Си.
Единственная неприятность — драйвер все строки возвращает в кодировке
Скачивалка с самарского архива
Несколько читателей сообщили, что моя скачивалка архивов ЭлАра не работает — самарский архив
Вчера мне понадобились документы оттуда, поэтому я её немного подпилил под новые реалии. Разрешение файлов пришлось убавить, но лично мне это дискомфорта не доставляет, а скачивается намного быстрее. В общем, новая версия лежит там же.
Чтобы вытащить из страницы список картинок, которые нужно качать, можно воспользоваться следующим способом: открываем консоль браузера и пишем туда следующее:
console.log("dxo.itemsValue=['"+
window['MainPlaceHolder__storageViewerControl__storageFilesViewerControl_FilesDropDownList_DDD_L'].
itemsValue.join("','")+"'];")Вывод нужно скопировать в файл и натравить на него скачиватель. Параметры остались неизменными.
Скачивалка документов из архивов ЭлАра (теперь можно указать страницы)
Программа для скачивания документов из архивов ЭлАра (инструкция есть у меня в блоге) неожиданно оказалась одной из самых популярных из того, что я делал.

Мне постоянно пишут благодарственные письма, а иногда — свои пожелания. Самое частое — сделать указание диапазона страниц для скачивания. Сегодня я эту возможность добавил, программа лежит на том же месте.
Скачивалка документов из архивов ЭлАра
Написала мне одна читательница по поводу моей утилиты для скачивания документов из электронных архивов ЭлАра. Оказывается, помимо самарского, моя утилита вполне нормально работает и с тобольским. Что ж, наверняка есть ещё архивы с которыми она совместима, поэтому в новую версию я добавил необязательный параметр host, который позволяет указать другой адрес

Утилита лежит там же — на «ГитХабе».
Как скачать документ из АИС ЦГАСО

Изучаю тут самарский электронный архив, который сделан в «Эларе». Насколько я понял, продукт называется АИС ЦГАСО. Убиваю сразу двух зайцев — разбираюсь как всё работает и пытаюсь найти нужные мне документы по родственникам.
Ребята из «Элара» молодцы, делают очень нужное дело — оцифровывают бумажные архивы, это невероятный труд, но, к сожалению, интерфейс просмотра документов оставляет желать лучшего, он мне сжёг массу нервов. Решил просто выкачать нужные мне документы на компьютер и дело с концом. Написал специальную программу для скачивания документов из АИС ЦГАСО, на Гугл Гоу. Получилось довольно компактно.
Основная фишка — паралельное скачивание документов в несколько потоков. Не уверен, что это действительно работает (я не до конца понимаю как Гоу переключает свои горутины), но в теории должно. Добавлено: «полевые испытания» подтвердили, что документы действительно скачиваются параллельно. Удалось сделать крайне просто:
ch := make(chan byte, N)
for i, id := range documents {
ch <- 1
go func(id string, index int) {
name := padZero5(strconv.Itoa(index)) + ".jpg"
copyUrlToFile(strings.Replace(url, `%id`, id, 1), *dir + "/" + name)
<-ch
}(id, i)
}Первой строкой в коде создаётся канал заданной размерности. С ним будут работать горутины (это такие легковесные потоки Гоу). В него можно положить только N значений, после чего следующая операция записи будет ждать его освобождения. С помещением в канал каждого нового значения, запускается горутина для скачивания одного листа документа, как только горутина отработала, она убирает из канала одно значение и цикл основного потока снова его кладёт и запускает новую горутину.
В итоге, одновременно, не будет запущено более N горутин.
Пользоваться программой просто (если
go run cgodownloader.go скачанный-файл.htmlПрограмма обработает документ, покажет сколько страниц найдено и начнёт их скачивать в виде больших джпегов в ту же папку, показывая их имена. У меня на большом документе (около 1,5 тысяч страниц) заняло прилично времени — час с лишним.

Зато теперь просматривать удобно.
30 банковских дней-2
Жена попросила во вчерашнюю программу ряд улучшений внести и пожаловалась, что она на одном из компьютеров на работе запускается медленно. Ну, я решил переписать её на Гоу, раз такое дело, может быстрее стартовать будет. Кроме того, мне всегда интересно посмотреть как одна и та же программа выглядит на разных языках программирования.
package main
import (
"encoding/json"
"os"
s "strconv"
t "time"
"net/http"
"io/ioutil"
"fmt"
r "regexp"
"bufio"
"strings"
fp "path/filepath"
)
type date struct {
D byte
M byte
Y int
}
func CreateDate(d, m string, y int) date {
day, _ := s.Atoi(d)
month, _ := s.Atoi(m)
return date{D:byte(day), M:byte(month), Y:y}
}
func (d date) FindIn(list []date) bool {
for _, cur := range list {
if d.D == cur.D && d.M == cur.M && (d.Y == cur.Y || cur.Y == 0) {
return true
}
}
return false
}
func (d date) ToTime() t.Time {
return t.Date(d.Y, t.Month(d.M), int(d.D), 0, 0, 0, 0, t.UTC)
}
func (d date) IsWeekEnd() bool {
dw := string(d.ToTime().Weekday())
return dw == "Saturday" || dw == "Sunday"
}
func (d date) Add(delta string) date {
dur, _ := t.ParseDuration(delta)
t := d.ToTime().Add(dur)
return date{D:byte(t.Day()), M:byte(t.Month()), Y:t.Year()}
}
func (d date) String() string {
return fmt.Sprintf("%02d.%02d.%04d", d.D, d.M, d.Y)
}
func loadCache(name string) (holidays []date) {
f, err := os.OpenFile(name, os.O_RDONLY, 0666)
if err != nil {
return nil
}
defer f.Close()
if err := json.NewDecoder(f).Decode(&holidays); err != nil {
return nil
}
return
}
func saveCache(name string, holidays []date) bool {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_TRUNC|os.O_CREATE, 0666)
if err != nil {
return false
}
defer f.Close()
if err := json.NewEncoder(f).Encode(&holidays); err != nil {
return false
}
return true
}
func getCurrentCalend() []byte {
if resp, err := http.Get("http://www.calend.ru/work/"); err == nil {
defer resp.Body.Close()
if body, err := ioutil.ReadAll(resp.Body); err == nil {
return body
}
}
return nil
}
func getCurrentHolidays() (dates []date) {
table, _ := r.Compile(`(?s)<table\s.+?time_of_death(.+)</table>`)
chunk, _ := r.Compile(`(?s)<td\s+class="\S+\s+col5"\s+day="(\d+)"\s+month="(\d+)"`)
dates = []date{}
year := t.Now().Year()
for _, value := range chunk.FindAllStringSubmatch(string(table.Find(getCurrentCalend())), -1) {
dates = append(dates, CreateDate(value[1], value[2], year))
}
return
}
func ReadUserInput(message string) string {
fmt.Print(message)
input, _ := bufio.NewReader(os.Stdin).ReadString('\n')
return input
}
func main() {
var currentholidays []date
year := t.Now().Year()
cachename := s.Itoa(year) + ".json"
// если кеш существует
if _, e := os.Stat(cachename); e == nil {
currentholidays = loadCache(cachename)
} else {
currentholidays = getCurrentHolidays()
saveCache(cachename, currentholidays)
}
// загружаем другие кеши
caches, _ := fp.Glob("*.json")
for _, name := range caches {
if name != cachename {
currentholidays = append(currentholidays, loadCache(name)...)
}
}
// опрашиваем пользователя
mask, _ := r.Compile(`^(?m)(\d+)\D+(\d+)`)
for {
var userdate date
for {
d := mask.FindStringSubmatch(ReadUserInput("Enter date (dd.mm): "))
if len(d) > 0 {
userdate = CreateDate(d[1], d[2], year)
break
}
}
var period int
for {
period, _ = s.Atoi(strings.TrimRight(ReadUserInput("Enter period: "), "\r\n"))
if period > 0 {
break
}
}
for ; period > 0; period-- {
for {
userdate = userdate.Add("24h")
if !userdate.FindIn(currentholidays) && !userdate.IsWeekEnd() {
break
}
}
}
fmt.Printf("Result: %s\n\n", userdate)
}
}Меня поразил размер скомпилированной программы. Вот эта мелочь под «Маком» занимает 3,9Мб, под «Виндой» — 3,7! Жена попросила убрать гуишное окошко, поэтому я сделал программу полностью консольной. Два разочарования: в регулярных выражениях под Виндой неверно определяется конец строки (не учитывается «\r», судя по всему) и второе — в Гоу нет ничего, чтобы работать с однобайтными кодировками, а в консоли Винды до сих пор CP866. Пришлось выводить всё
Объём увеличился,
&^ в Google Go
Отличная иллюстрация того как важны мелочи в документации…
Читал чужой исходный код на «Гоу» и обнаружил необычную конструкцию:
v &^= 1 >> nПочесал голову, не понял что это и пошёл читать документацию. Выяснилось, что эту конструкцию я
package main
func main() {
var test byte = 250
println(test) // 250 (11111010)
test = ^test
println(test) // 5 (00000101)
test &^= 4 // (00000100)
println(test) // 1 (00000001)
}Алгоритм линейного счётчика на Google Go (golang)
Не смог отказать себе в удовольствии и попрактиковаться в «Гоу», написал и выложил на Гитхаб реализацию алгоритма линейного счётчика на Гугл Гоу. Значения, которые надо подсчитать поступают на стандартный вход, есть один параметр командной строки — «size», позволяющий задать размер вектора в битах

Подсчёт на моём ноуте одного миллиона уникальных значений занимает примерно 6,5 секунд. Погрешность с вектором длиной 100 тысяч бит (≈3КиБ) — 7,6%.
Чтобы скомпилировать программу новым (версии 1 и выше) компилятором «Гоу», нужно запустить следующую команду:
go build linear-counter.goРазворот односвязного списка за O(n) (память — O(1)) на Гугл Гоу
Вчера друг подвозил меня до дома, я решил, пока в пробке, попрограмировать на «Гоу». Практики давненько не было, язык уже начинает забываться, не модули, это ерунда, а сами конструкции, вот что неприятно.
Взял задачу — развернуть односвязный список за O(n) с константным ограничением на память. Задачу частенько дают на собеседованиях, как я слышал, говорят мало кто решает, решил посмотреть действительно ли это сложно. В уме прикинул, подумалось что нет, практика показала, что я не ошибся. В общем, выкладываю что получилось.
package main
import (
"os"
"bufio"
)
// тип для односвязного списка
type links struct {
Next *links
Value string
}
// вывод на печать односвязаного списка
func (chain *links) Print() {
for chain != nil {
os.Stdout.WriteString(chain.Value)
chain = chain.Next
}
}
// разворачиваем односвязный список за O(n)
func (chain *links) Reverse() *links {
if chain == nil || chain.Next == nil {
return chain
}
left, next := chain, chain.Next.Next
chain = chain.Next
left.Next = nil
for {
chain.Next = left
if next == nil {
break
}
left, chain, next = chain, next, next.Next
}
return chain
}
// Создаём список из чего-нибудь, умеющего выдавать по одной строке за раз
func CreateFromReader(readline func() *string) *links {
var chain, start *links = nil, nil
for {
if line := readline(); line != nil {
link := &links{ nil, *line }
if chain == nil {
chain, start = link, link
} else {
chain.Next, chain = link, link
}
} else {
return start
}
}
return nil
}
// Точка входа
func main() {
if len(os.Args) < 2 {
os.Exit(1)
}
f, err := os.OpenFile(os.Args[1], os.O_RDONLY, 0666)
if err != nil {
os.Exit(2)
}
defer f.Close()
r := bufio.NewReader(f)
CreateFromReader(
func() (*string) {
if line, e := r.ReadString('\n'); e == nil {
return &line
}
return nil
},
).Reverse().Print()
}Список создаётся из файла, который нужно передать программе первым параметром. Вообще, вся реализация разворота — в методе Reverse класса links, остальное — заполнение структуры и вывод на печать. Оцените как мало надо кода, чтобы решить это задание.
Google Go GD binding
Исправил свой биндинг графической библиотеки GD к языку «Гоу» так, чтобы он работал под первой версией языка. Теперь изменился способ его установки и использования.
Ставить его теперь намного проще, достаточно выполнить следующую команду (в системе должна быть установлена библиотека GD):
go get github.com/bolknote/go-gdСпособ подключения бибилиотеки так же изменился (см. sample.go):
import gd "github.com/bolknote/go-gd"Надеюсь, больше «Гугл» библиотеки языка и способ сборки менять не будет.
Добавлено позднее: заодно портировал на Go 1 и свою программу MakeCorner, ставиться, опять же, стала попроще, а вот размер сильно вырос. Версия r60 компилировала файл размером 1,6МБ, версия 1 компилирует уже в 2,2МБ. Безобразие!
Ещё недостаток «Гоу»
В отличие от многих языков, в «Гоу» символ решётки не позволяет комментировать строку. В упомянутых языках это сделано не зря. В различных
Сам же язык, встретив первой строкой
В «Гоу» такого нет. Несмотря, что в первой версии появилась команда «go run», позволяющая запускать программы из командной строки, не компилируя их (на самом деле компилируя, но результат не сохраняется), я не могу указать в первой строке «#!/usr/local/bin/go run», «Гоу» меня отругает:
bolk@dhcp-174 ~ $ ./main.go
package :
main.go:1:1: illegal character U+0023 '#'Прискорбно.
Переход на «Гоу 1»
С выходом первой версии языка программирования «Гоу» посмотрел как компилируются мои программы. Не компилируются никак. Весь набор утилит поменяли, теперь всё делается через утилиту «go». С одной стороны хорошо — раньше, чтобы просто запутсить код нужно было много танцев проделать, а сейчас «go run code.go», и всё, он работает.
С другой стороны, мне все make-файлы придётся переписать, надо ещё разобраться как всё теперь работает, но самое плохое вот что:
Finally, the Go tool chain (compilers, linkers, build tools, and so on) are under active development and may change behavior. This means, for instance, that scripts that depend on the location and properties of the tools may be broken by a point release.
Для тех, кто не понимает английский — набор утилит «Гоу» находится в стадии разработки и ещё может измениться. Это плохо очень. Я не хотел бы с выходом каждой новой версии смотреть не сломалось ли у меня
Гоу вышел
Today marks a major milestone in the development of the Go programming language. We’re announcing Go version 1, or Go 1 for short, which defines a language and a set of core libraries to provide a stable foundation for creating reliable products, projects, and publications.
Появился релиз языка «Гоу». Это означает, что язык стабилизировался и обратную совместимость ломать больше не будут.
Многозадачность в «Гоу»
В прошлой своей записи я упомянул многозадачность в «Гоу», а сейчас вспомнил, что я ни разу не показывал как же она там выглядит с точки зрения простого программиста. Покажу на примере.
Предположим, нам нужно вычислить
package main
import (
"io"
"os"
"crypto/sha512"
"encoding/hex"
"runtime"
"strconv"
)
func main () {
ch := make(chan string)
N := 3
runtime.GOMAXPROCS(N)
for i := 0; i<N; i++ {
go func (name string, num int, ch chan string) {
if f, err := os.OpenFile(name, os.O_RDONLY, 0666); err == nil {
defer f.Close()
h := sha512.New()
io.Copy(h, f)
ch <- strconv.Itoa(num) + ". " + hex.EncodeToString(h.Sum())
} else {
panic("Cannot open file")
}
}(os.Args[0], i, ch)
}
for ; N > 0; N-- {
print(<-ch, "\n")
}
}Что здесь происходит.
Первая строка объявляет имя модуля, для основного модуля программы это имя должно быть «main», эта конструкция скажет компилятору, что именно тут находится входная точка нашей программы. На первый взгляд кажется, что в основном модуле можно было бы просто не указывать имя, но язык придерживается хорошего принципа «явное лучше неявного», чем он мне очень симпатичен.
Дальше, очевидно, подключаются модули, в языке базовых функций почти нет, поэтому даже для такой небольшой программы мне понадобилось шесть модулей: «io» для копирования данных из одного потока в другой, «os» для работы с файлом, «crypto/sha512» для хеширования, «encoding/hex» для кодирования получившейся
Функция main — входная точка основного модуля программы, именно она (как в «Си») вызывается в самом начале.
Далее я создаю (make) небуферизированный «канал» и объявляю, что по нему будут идти строки, конструкция «:=» многим напоминает Паскаль, но отношения к нему не имеет, это синтаксический сахар для создания локальной переменной. «Канал» — это хорошее, говорящее название, хорошо отображающее суть, это действительно канал общения
В цикле создаются и сразу запускаются в фоне (конструкцией «go») на выполнение N
Внутри
Цикл не ждёт выполнения
Дальше идёт второй цикл, где N раз запускается команда на чтение из канала („… <- ch»).
Чтобы убедиться, что
У такой лёгкости использования есть свои ограничения, но об этом
Гоу версия 1
Язык программирования «Гугл Гоу»
We propose to issue a Go release early next year that will be called “Go version 1”, Go 1 for short, that will be the first Go release to be stable in this way. Code that compiles in Go version 1 should, with few exceptions, continue to compile throughout the lifetime of that version, as we issue updates and bug fixes such as Go version 1.1, 1.2, and so on… Go version 1 will be a stable language with stable libraries. Other than critical fixes, changes made to the library and packages for versions 1.1, 1.2 and so on may add functionality but will not break existing Go version 1 programs.
Опять столкнулся с изменившимся АПИ «Гугл Гоу»
Обновил компилятор до r59 и сразу пришлось менять код моего «МэйкКорнера — в модуле strings метод Split теперь называется SplitN, а Split делает то же самое, что SplitN с последним параметром
Часы без JavaScript: фильм о фильме
Как уже догадались все, кому это было хоть каплю интересно, часы без JavaScript сделаны анимированным GIF. Сделал я всё очень просто: скачал найденный

Оказалось, что проще всего сгенерировать картинки при помощи Go. В Пайтоне на «Маке» у меня нет PIL и я его плохо знаю, во встроенном PHP нет поддержки TTF, так что Go:
package main
import "gd"
import "os"
import "fmt"
func main() {
im := gd.CreateTrueColor(21, 21)
white := im.ColorAllocate(255, 255, 255)
black := im.ColorAllocate(0, 0, 0)
// стрелки: 64 → 123 (m) и 192 → 251 (h)
defer im.Destroy()
for i := 0; i < 720; i++ {
im.FilledRectangle(0, 0, 20, 20, white)
min := i % 60
hour := (i / 60) * 5 + min / 12
im.StringFT(black, "./clock.ttf", 10, 0, 0, 16, "1")
im.StringFT(black, "./clock.ttf", 10, 0, 0, 16, string(hour + 192))
im.StringFT(black, "./clock.ttf", 10, 0, 0, 16, string(min + 64))
name := fmt.Sprintf("gifs/%02d%02d.gif", i/60, i%60)
im.Gif(name)
}
}После запуска получилось 720 картинок с ноль часов ноль минут до 11:59 (я не зря выбрал часы со стрелками, это позволило сократить размер анимированных часов вдвое).
Когда на сервер приходит запрос, я проверяю сколько сейчас времени и, если файл с часами для текущей минуты уже существует, то делаю редирект на существующую картинку. Если нет, то генерирую из 720 картинок новый «мультик», кладу его на диск и делаю редирект на него (остальные картинки удаляю):
<?
include "clock.inc.php";
list($h, $m) = explode(':', date('H:i'));
if ($h > 12) $h -= 12;
$name = sprintf("%02d%02d.gif", $h, $m);
if (!file_exists($name)) {
@array_map('unlink', glob('*.gif'));
$frames_tail = $frames = $framed = array();
$frames_name = 'frames_tail';
for ($H = 0; $H<12; $H++) {
for ($M = 0; $M<60; $M++) {
if ($h == $H && $m == $M) {
$frames_name = 'frames';
}
${$frames_name}[] = sprintf("gifs/%02d%02d.gif", $H, $M);
$framed[] = 100;
}
}
$frames = array_merge($frames, $frames_tail);
$gif = new GIFAnimator($frames, $framed, 0, 2, -1, -1, -1, 'url');
fwrite(fopen($name, 'wb'), $gif->GetAnimation());
}
header("Location: $name");Для генерации анимированных GIF применяется