将 HTML 转换为 PNG,宽度取决于本地内容

Convert HTML to PNG with width dependent on content locally

我需要根据文档的内容将 HTML 文档转换为 PNG。 HTML 文档包含一张图像,其样式取决于 image/document 的大小。我一直在研究和尝试解决方案,但 none 非常适合我的需求。例如,如果一个 HTML 文档包含一个 500x500 的图像和它下面的一些文本,我希望输出为 500 像素宽以及图像和文本的高度。

wkhtmltoimage 是我找到的最接近这样的程序。它具有我需要的智能调整大小功能(只需将宽度设置为 1 并让它扩展以填充),但它基于非常旧版本的 webkit。它不支持 CSS3 calc()vw。此外,它在 ECMAScript 5 上。尽管 ECMAScript 5 有很多方法来获取文档的宽度,但 wkhtmltoimage 不支持其中任何一种,它们都是 return 0。我的文本大小取决于宽度文件,所以我需要支持。

我发现的所有其他解决方案似乎都不支持智能调整大小,因为它们基于无头浏览器。然而,我可能误解了这些,他们可能支持我正在寻找的东西。

对于那些好奇的人,我的实际实现是在一个 python 脚本中,该脚本将字符串 HTML 文档通过管道传输到程序中,并将 png 发送到程序的其他部分。不过,我不介意在那里工作。

TL;DR 是否有一些 local 程序可以实现我想要的:将 HTML 文档转换为支持 vw 的 PNG 文件,calc,以及智能宽度?

这是我能够创建的最佳解决方案。它使用硒和铬。需要注意的非常重要的一点:因为启动 selenium 需要很长时间,所以我只初始化它一次。确保如果您在 asyncio 循环或其他东西中执行此代码以获取锁,那么它一次只做一件事。

此外,您需要修改 temp_file() 功能或在工作文件夹中创建 temp/ 目录。

这意味着在它自己的文件中,在导入后使用其他文件中的 html2png()。如前所述,启动 selenium 需要一段时间,因此在导入时它会停顿几秒钟。

import json
import os
import random
import string
import sys

from selenium import webdriver


def send(driver, cmd, params=None):
    if params is None:
        params = {}
    resource = "/session/%s/chromium/send_command_and_get_result" % driver.session_id
    url = driver.command_executor._url + resource
    body = json.dumps({'cmd': cmd, 'params': params})
    response = driver.command_executor._request('POST', url, body)
    # if response['status']: raise Exception(response.get('value'))
    return response.get('value')


def get_random_string(length):
    return ''.join(random.choice(string.ascii_letters) for i in range(length))


def temp_file(extension="png"):
    while True:
        name = f"temp/{get_random_string(8)}.{extension}"
        if not os.path.exists(name):
            return name


def loadhtml(driver, html):
    base = "file:///" + os.getcwd().replace("\", "/")
    # html = html.replace("<base href='./'>", f"<base href='{base}/'>")
    html = f"<base href='{base}/'>" + html
    file = temp_file("html")
    with open(file, "w+") as f:
        f.write(html)
    driver.get("file:///" + os.path.abspath(file).replace("\", "/"))
    return file
    # print(json.dumps(html))
    # print(html)
    # html_bs64 = base64.b64encode(html.encode('utf-8')).decode()
    # driver.get("data:text/html;base64," + html_bs64)


opts = webdriver.ChromeOptions()
opts.headless = True
opts.add_experimental_option('excludeSwitches', ['enable-logging'])
opts.add_argument('--no-proxy-server')
opts.add_argument("--window-size=0,0")
opts.add_argument("--hide-scrollbars")
opts.add_argument("--headless")
opts.add_argument("--disable-web-security")
opts.add_argument("--allow-file-access-from-files")
opts.add_argument("--allow-file-access-from-file")
opts.add_argument("--allow-file-access")
opts.add_argument("--disable-extensions")
# https://chromedriver.storage.googleapis.com/index.html?path=87.0.4280.88/
if sys.platform == "win32":
    driver = webdriver.Chrome("chromedriver87.exe", options=opts)
else:
    driver = webdriver.Chrome("chromedriver87", options=opts)


def html2png(html, png):
    driver.set_window_size(1, 1)
    tempfile = loadhtml(driver, html)
    func = """
            function outerHeight(element) {
        const height = element.offsetHeight,
            style = window.getComputedStyle(element)

        return ['top', 'bottom']
            .map(function (side) {
                return parseInt(style["margin-"+side]);
            })
            .reduce(function (total, side) {
                return total + side;
            }, height)
    }"""
    size = driver.execute_script(f"{func};return [document.documentElement.scrollWidth, outerHeight(document.body)];")
    driver.set_window_size(size[0], size[1])
    size = driver.execute_script(f"{func};return [document.documentElement.scrollWidth, outerHeight(document.body)];")
    driver.set_window_size(size[0], size[1])
    send(driver, "Emulation.setDefaultBackgroundColorOverride", {'color': {'r': 0, 'g': 0, 'b': 0, 'a': 0}})
    driver.get_screenshot_as_file(png)
    os.remove(tempfile)

# html2png("<p>test</p>", "test.png")