вторник, 29 ноября 2016 г.

Sigil Image downloader plugin

К сожалению, вопрос написания плагинов для Sigil очень мало освещён. Нужно постараться, чтобы найти даже официальный мануал, содержащий скупые описания. И ни они, ни исходники не отвечают на некоторые интересные вопросы.

Полезные ссылки, все на английском:

https://github.com/Sigil-Ebook/Sigil/blob/master/docs/Sigil_Plugin_Framework_rev7.epub - официальный мануал.
http://www.mobileread.com/forums/attachment.php?attachmentid=140885&d=1439113322 - PDF версия с некоторыми примерами.
https://github.com/Sigil-Ebook/Sigil/blob/master/src/Resource_Files/plugin_launchers/python/bookcontainer.py - Один из самых важных заголовочных файлов
Заголовки https://github.com/Sigil-Ebook/Sigil/tree/master/src/Resource_Files/plugin_launchers/python

Результат:
Исходный код: https://github.com/yastrov/imagedownloader-sigil-plugin
Релиз: https://github.com/yastrov/imagedownloader-sigil-plugin/releases

Итак:
Периодически есть желание сделать копию статьи из интернета, и часто там присутствуют изображения - фотографии, картинки, иллюстрации.
Можно с одной стороны сохранять целиком веб-страничку. А можно написать плагин, и просто копировать нужный фрагмент статьи (или её всю целиком) в Sigil и просто запустить плагин. К счастью, авторы не стали изобретать что-то странное и взяли привычный и простой Python.

Интерпретатор версии 3.4 встроен в версию Sigil для Windows (поставляется вместе), и содержит много полезных библиотек, например PIL, html5lib, regex, lxml, библиотеки для работы с CSS и другие.
В версии для  Windows х64 библиотеки можно увидеть по адресу (по умолчанию): C:\Program Files\Sigil\python3\Lib\site-packages
Для х32 - C:\Program Files (x86)\Sigil\python3\Lib\site-packages
Но можно использовать и свой интерпретатор, указав его в настройках "Управление модулями" Sigil (установка плагинов происходит через неё же, почитать про установку плагинов можно в мануале или здесь: http://www.rshelton.org/2015/10/new-version-of-sigil.html).

Чтобы плагин был плагином, нужно создать plugin.xml с нехитрым содержимым (Его можно будет посмотреть или в мануалах, или в репозитории на GitHub). Единственная особенность: не стоит пытаться указывать совместимость с python3.5 - плагин просто не загрузится.
Только с python3.4 или python2.7 (Рекомендован всё же 3.4).

Сам скрипт, главная часть плагина - имеет имя plugin.py.

Входной точкой в плагин будет функция run, принимающая объект класса BookContainer с именем bk и возвращающая код завершения (успеха) - 0:

def run(bk):
    pass
    # наши действия будем писать здесь.
    return 0

Все остальные функции либо будут вызываться из run, либо написаны "на всякий случай, если скрипт запустят из терминала, без загрузки в Sigil".

Чтобы обойти все файлы с текстом (html, а точнее xhtml странички), мы воспользуемся, как и советуют, следющей конструкцией:

for (_id, href) in bk.text_iter():
    html = bk.readfile(_id)
    if not isinstance(html, text_type):
        html = str(html, 'utf-8')
    html_orig = html
    # наши действия
    if not html == html_orig:  # Проверяем, что содержимое файла наш скрипт изменил
        bk.writefile(_id, html)

С помощью bk.text_iter() обойдём все странички с текстом книги, получая для каждой _id - идентификатор (по факту - имя файла в стиле Section0001.xhtml), и адрес (например: ../Text/Section0001.xhtml). _id Мы потом используем для сохрания текста с помощью bk.writefile(_id, html).

Чтобы выяснить, какие файлы изображений уже есть в книге, используем bk.image_iter():
exists_image_id_list = list(_id for _id, _, _ in bk.image_iter())

Казалось бы. Но не всё так просто - поднимается вопрос "как сохранять наше изображение в книгу"?
Sigil API, а точнее bookcontainer.py предлагает функцию addfile класса BookContainer.
addfile требует загадочный параметр uniqueid, basename и data.
data - это содержимое нашего файла, бинарные данные.
basename - по логике имя файла без пути, т.е. скажем image.png
А uniqueid? Мануал молчит. Экспериментально, (а заодно подсмотрев в те id, что возвращает text_iter() ), было установлено - в этом качестве прекрасно подходит имя файла (то же самое basename).
И фактически (в исходниках используется uniqueid, но смысл тот же) получаем вызов:
bk.addfile(basename, basename, data)
Остальное Sigil сделает самостоятельно.

Проверить существование uniqueid можно либо попытавшись загрузить файл из книги bk.readfile,
либо вызвав bk.basename_to_id(uniqueid), которая вернёт либо None, если такого id ещё не встречалось, либо значение uniqueid, если уже существует и был использован.

Остальное - дело техники:
- Найти ссылки на странице с помощью простого регулярного выражения:
urls = re.findall(r'<img.+?src=["\'](http.+?)["\']', html, re.I + re.M)
- пройтись по каждой из них, получая адреса файлов банальным url[url.rfind('/') + 1:]
- загрузить файл с помощью штатного urllib.request.urlopen
* Для этого создана функция get_data(url), строка 16 скрипта *
* Да, действительно есть прекрасный requests, но это добавляет скрипту лишнюю зависимость. А так хоть и без поддержки сессий HTTP 2, но зато работает из коробки, в том числе на встроенном в Sigil интерпретаторе.*
- Проверить, есть ли у нас такой же файл
* Для проверки используется хэш сумма SHA512 так же из штатной библиотеки:
 hashlib.sha512(data).digest(), которая сравнивается с тем файлом, уже существующим в книге, у которого совпадает имя (оно же uniqueid) *
- Создать уникальное имя файла, и проверить его
* Для этого создана функция make_unique_id_for_img(unique_id, exists_image_id_list), строка 35*
- Записать изображение с помощью bk.addfile
- Исправить ссылку на файл, заменив внешнюю (из интернета), на локальную "../Images/%uniqueid%" банальным html.replace
- Сохранить наш html: bk.writefile(_id, html)
- Добавить обработку ошибок и вывод информации о том, что сейчас происходит.
- Запаковать в ZIP архив, при этом архив должен называться так же, как и папка (со скриптом и XML файлом), которая в него вложена.

Результат:
Исходный код: https://github.com/yastrov/imagedownloader-sigil-plugin
Релиз: https://github.com/yastrov/imagedownloader-sigil-plugin/releases