Конвертировать SVG в PNG с поддержкой преобразования CSS3 matrix3d

Мне нужно конвертировать SVG в PNG из скрипта Python. Для этого есть множество инструментов, которые я пробовал (я на Ubuntu):

  • inkscape (Командная строка Inkscape)
  • rsvg-convert от librsvg2-bin
  • convert (это ImageMagick)

Но ни один из них не поддерживает CSS3 transform: matrix3d(...), Единственное программное обеспечение, которое поддерживает это, я обнаружил до сих пор, это Firefox/Chrom[ium]/etc, но я, похоже, не разрешаю рендеринг командной строки в PNG.

Есть ли какие-либо специальные опции, которые я мог бы передать одному из вариантов выше, чтобы получить рендеринг с полной поддержкой CSS3? Или есть еще один вариант конвертации, о котором я сейчас не знаю?


Редактировать Я попробовал больше инструментов, в том числе:

  • wkhtmltoimage часть http://wkhtmltopdf.org/
  • nodehot (с большим количеством слесарного дела, чтобы обслуживать SVG на локальном сервере и затем загрузить окончательное изображение)

И, возможно, хотя я не мог проверить, потому что это только OS X

  • webkit2png (спасибо за предложение, @Mark Setchell)

Все вышеперечисленное не соответствует моему требованию, потому что они основаны на WebKit, а WebKit просто не поддерживает matrix3d ​​в ​​SVG, хотя он отлично подходит для обычных элементов...

1 ответ

Ура, я нашел решение - но оно имеет огромные накладные расходы и требует множества вещей:

Основная идея состоит в том, чтобы обслуживать ваш SVG в качестве веб-сайта, а затем отображать его с помощью SlimerJS, версии Firefox со скриптами. В отличие от других упомянутых подходов, для визуализации SVG используется Gecko, и, как упоминалось выше, Gecko (в отличие от WebKit) корректно отображает 3D-вращения CSS3 в SVG.

Возможно, вы тоже захотите использовать xvfb, поэтому вам не нужно видеть окно SlimerJS во время рендеринга (оно пока не поддерживает безголовый).

Служите SVG/HTML на локальном сервере

Во-первых, вам нужно представить свой SVG как изображение на странице HTML. Встроенный SVG или обычный SVG у меня не сработали. рекомендую http.server.BaseHTTPRequestHandler, обслуживающий как HTML, так и обычный SVG (который будет запрошен во втором запросе).

html = """
<!DOCTYPE HTML>
<html>
    <head>
        <style>
            body {
                margin: 0;
            }
        </style>
    </head>
    <body>
        <img src="http://localhost:8000/svg/%s" />
    </body>
</html>
""" % svg_name

margin: 0; удаляет пространство по умолчанию вокруг любого веб-сайта.

Я запускаю сервер как Thread с deamon=True поэтому он будет закрыт, когда мой сценарий будет завершен.

class SvgServer:
    def __init__(self):
        self.server = http.server.HTTPServer(('', PORT), SvgRequestHandler)
        self.server_thread = threading.Thread(target=self.server.serve_forever, daemon=True).start()

SvgRequestHandler должен быть вашим экземпляром BaseHTTPRequestHandler

(Я предполагаю, что есть или будет способ прямого доступа к файлам из SlimerJS, потому что Firefox может сделать это с file:// но я не мог заставить его работать. Тогда этот шаг станет устаревшим.)

Рендеринг с помощью SlimerJS

Теперь, когда SVG доступен для браузера, мы можем назвать SlimerJS. SlimerJS принимает только файлы JavaScript в качестве входных данных, поэтому нам лучше сгенерировать немного JavaScript:

slimer_commands = """
var webpage = require('webpage').create();
webpage
  .open('%s')
  .then(function () {
    webpage.viewportSize = { width: 1920, height: 1080 };
    webpage.render('%s', { onlyViewport: true });
    slimer.exit()
  });
""" % (url_for_html_embedding_svg, output_file_name)

Бонус: пакетная обработка с Promises, это намного быстрее по сравнению с запуском отдельного SlimerJS для каждого SVG, который мы хотим визуализировать. Я лично работаю с индексированными SVG, меняю по мере необходимости.

slimer_command_head = "const { defer } = require('sdk/core/promise');" \
                      "var webpage = require('webpage').create();" \
                      "webpage.viewportSize = { width: 1920, height: 1080 };" \
                      "var deferred = defer();" \
                      "deferred.resolve();" \
                      "deferred.promise.then(function () {"

commands = [slimer_command_head]

for frame_index in range(frame_count):
    command = "return webpage.open('%s'); }).then(function () { webpage.render('%s', { onlyViewport: true });" % (
        'http://localhost:8000/html/%d' % frame_index,
        FileManagement.png_file_path_for_frame(frame_index)
    )

    commands.append(command)

commands.append("slimer.exit(); });")

slimer_commands = ''.join(commands)

Теперь, когда у нас есть готовый скрипт, сохраните его в временном файле и выполните его:

with tempfile.NamedTemporaryFile(suffix='.js') as slimer_file:
    slimer_file.write(bytes(slimer_commands, 'UTF-8'))
    slimer_file.flush()

    command = [
        SLIMER_EXECUTABLE,
        os.path.abspath(slimer_file.name)
    ]

    if run_headless:
        command.insert(0, 'xvfb-run')

    os.system(' '.join(command))

run_headless опция добавляет команду XVFB, чтобы перейти без головы.

Вы сделали

Это было легко, быстро и прямо, не так ли?

Если вы не могли по-настоящему следовать фрагментам кода, посмотрите исходный код проекта, для которого я его использовал.

Другие вопросы по тегам