Firefox headless HTML to image

Sziasztok!

Adott egy szerver, PHP előállít HTML állományokat, van hozzá külön CSS, szépen néz ki minden böngészőben. A feladat az, hogy a HTML DOM bizonyos DIV-jeiből képet készítsek, amely képeket a PHP program majd előnézeti ábrához felhasználna.

Próbáltam a Phantomjs-t, de értékelhetetlenül csúnya végeredményt adott. Nézem most a Firefox-ot, elvileg tud screenhot-ot készíteni URL alapján, de nem hajlandó ugyanezt megtenni ha file:///home/xyz/test.html-lel próbálkozom.

Teljesen tanácstalan vagyok, mi legyen a megoldás az előnézeti képek generálására a szerver oldalon.

Barátkozom a Google-val, de nem a barátom. Nem hiszem, hogy csak nekem lenne szükségem ilyen dologra.

Nektek lenne ötletetek?

 

Cz

Hozzászólások

Ne selenium ide-t nézd! Az semmire nem való...

Rajk össze egy selenium grid-et.

https://github.com/eficode/Docker-Selenium-Example/blob/master/docker-compose.yml

codeception-on keresztül gyerekjáték elérni. Sőt itt még mozizni is tudsz egy vnc-n keresztül. Én sokszor használom népvakítása. 

https://codeception.com/docs/modules/WebDriver

Majd itt használd a makescrenshot-ot

A végén még a node.js is lehet befutó, de jelenleg ismerkedem a témával. Két nappal ezelőttig azt se tudtam, hogy létezik a webkit2gtk és csomó remek dolgot lehet vele művelni, leszámítva épp azt, ami a feladat lenne :-) Keresgélek, csapongok, próbálom megérteni a programok működését, igyekszem felderíteni a dokumentációból a lényeget.

Szerkesztve: 2020. 02. 21., p – 21:17

> Nézem most a Firefox-ot, elvileg tud screenhot-ot készíteni URL alapján, de nem hajlandó ugyanezt megtenni ha file:///home/xyz/test.html-lel próbálkozom.

De localban miért nem egy a szerveredhez hasonló környezetben dolgozol, vagy legalább egy webszerveren? Aztán https://example.com.tld localban.

Szerk.:
Most esett le, hogy van ott PHP, stb. Miért a file:///home/xyz/test.html az útvonal?

Biztos, hogy nem én gonoszoltam le!

Csak próbálkozom, de van észszerűség abban, hogy http://-n keresztül érjem el a HTML állományokat. Jé. és tényleg :-)

No mármost, az a következő bajom, hogy a szerveren (Ubuntu 18.04) nincs grafikus környezet. Ha felteszem a firefox-ot, akkor magával húzza az ablakkezelőt is? Hogy kéne telepíteni a Firefox-ot, hogy minél kevesebb egyéb sallangot kelljen feltelepíteni?

Szerkesztve: 2020. 02. 22., szo – 02:03

bongeszo-kepmento.py

Usage:   bongeszo-kepmento.py src_url dst_png_path render_width
Example: bongeszo-kepmento.py https://hup.hu hup-mentes.png 1200

További beállítások a programon belül: "headless" és "snapshot_opts"

Ha headless=True és a webkit-nek nem tetsző, nem komplett URI-t adtál, akkor "kill bongeszo-kepmento.py". (headless=False esetén elég Alt+F4 a megnyíló ablakon)

Telepítendő csomagok (Ubuntu 18.04.mai elnevezések):

  • python3-gi
  • gir1.2-webkit2-4.0
  • libwebkit2gtk-4.0-37  (ezt Ubuntu-n behúzza a előző, talán más rendszeren nem)
#!/usr/bin/env python3

import sys
print("Python executable:", sys.executable)
print("Python version:   ", sys.version.replace('\n', ' '))

import os
print("Python venv:      ", end=' ')
try:
    print(os.environ['VIRTUAL_ENV'])
except:
    print("not in venv")

print("Python path:      ", *sys.path, sep = '\n\t')


import gi
print("Module gi path:   ", gi.__file__)

gi.require_version('Gtk', '3.0')
gi.require_version('WebKit2', '4.0')
from gi.repository import Gtk, Gdk, WebKit2

ctx = WebKit2.WebContext()

# Command line arguments
if 4 != len(sys.argv):
    print("Usage:   bongeszo-kepmento.py src_uri dst_png_path render_width")
    print("Example: bongeszo-kepmento.py https://hup.hu hup-mentes.png 1200")
    exit(-1)
src_uri = sys.argv[1]
dst_png_path = sys.argv[2]
render_width = int(sys.argv[3])

# Tweak "headless" and "snapshot_opts" as you like
headless = False
if headless:
    base = Gtk.OffscreenWindow
else:
    base = Gtk.Window

snapshot_opts = WebKit2.SnapshotOptions.TRANSPARENT_BACKGROUND
#snapshot_opts = WebKit2.SnapshotOptions.NONE


class Browser(base):
    def __init__(self, uri):
        super(Browser, self).__init__()
        
        self.webview = WebKit2.WebView.new_with_context(ctx)
        self.webview.connect("load-changed", self.on_load_changed)
        self.webview.load_uri(uri)
        self.add(self.webview)

        self.connect("destroy", Gtk.main_quit)
        self.set_size_request(render_width, 600) # set minimum size allowed

        self.set_title("Bongeszo - " + uri)
        self.show_all()
    
    
    # WebView events
    
    def on_load_changed(self, web_view, load_state):
        print("Load-state:", load_state)
        if load_state == WebKit2.LoadEvent.FINISHED:
            print("Taking snapshot ...")
            web_view.get_snapshot(WebKit2.SnapshotRegion.FULL_DOCUMENT, snapshot_opts, cancellable=None, callback=self.on_snapshot_finish)
    
    def on_snapshot_finish(self, web_view, result):
        print("Snapshot ready")
        cairo_surf = web_view.get_snapshot_finish(result)
        cairo_surf.write_to_png(dst_png_path)
        print("Snapshot saved to", dst_png_path)
        self.close()
        
if __name__ == "__main__":
    Gtk.init(sys.argv)
    Browser(src_uri)
    Gtk.main()

Támadt némi hibaüzenetem:

Python executable: /usr/bin/python3
Python version:    3.6.9 (default, Nov  7 2019, 10:44:02)  [GCC 8.3.0]
Python venv:       not in venv
Python path:      
	/home/xyz
	/usr/lib/python36.zip
	/usr/lib/python3.6
	/usr/lib/python3.6/lib-dynload
	/usr/local/lib/python3.6/dist-packages
	/usr/lib/python3/dist-packages
Module gi path:    /usr/lib/python3/dist-packages/gi/__init__.py
Unable to init server: Could not connect: Connection refused
Unable to init server: Could not connect: Connection refused
Unable to init server: Could not connect: Connection refused
EGLDisplay Initialization failed: EGL_NOT_INITIALIZED
WaylandCompositor requires eglCreateImage and eglDestroyImage.
Nested Wayland compositor could not initialize EGL
Unable to init server: Could not connect: Connection refused

(htmlscr.py:8768): Gtk-WARNING **: 06:52:04.540: cannot open display: 

Hogyan lehet ezt meggyógyítani?

Ez jó tanács volt, egy kicsit tovább tudtam jutni:

 

Python executable: /usr/bin/python3
Python version:    3.6.9 (default, Nov  7 2019, 10:44:02)  [GCC 8.3.0]
Python venv:       not in venv
Python path:      
	/home/xyz
	/usr/lib/python36.zip
	/usr/lib/python3.6
	/usr/lib/python3.6/lib-dynload
	/usr/local/lib/python3.6/dist-packages
	/usr/lib/python3/dist-packages
Module gi path:    /usr/lib/python3/dist-packages/gi/__init__.py
Load-state: <enum WEBKIT_LOAD_STARTED of type WebKit2.LoadEvent>
Load-state: <enum WEBKIT_LOAD_REDIRECTED of type WebKit2.LoadEvent>
Load-state: <enum WEBKIT_LOAD_REDIRECTED of type WebKit2.LoadEvent>
Load-state: <enum WEBKIT_LOAD_COMMITTED of type WebKit2.LoadEvent>
failed to create drawable
Load-state: <enum WEBKIT_LOAD_FINISHED of type WebKit2.LoadEvent>
Taking snapshot ...
Snapshot ready
Traceback (most recent call last):
  File "htmlscr.py", line 72, in on_snapshot_finish
    cairo_surf = web_view.get_snapshot_finish(result)
TypeError: Couldn't find foreign struct converter for 'cairo.Surface'

^CGdk-Message: 15:04:08.880: htmlscr.py: Fatal IO error 11 (Resource temporarily unavailable) on X server :99.

Úgy néz ki, ezzel van most baja:

 

        cairo_surf = web_view.get_snapshot_finish(result)

Telepítettem egy python3-cairo csomagot, de semmi változás a kimenetben.

Mester, hogyan tovább? :-)

Akkor meg is válaszolnám a kérdésemet:

  • apt-get install python3-gi
  • apt-get install gir1.2-webkit2-4.0
  • apt-get install python3.6-gi
  • apt-get install xvfb
  • apt-get install python3-cairo
  • apt-get install python3-gi-cairo

Ezeket kellett telepíteni, majd így futtatni:

xvfb-run python3 htmlscr.py http://bme.hu bme.png 1200

Most már csak ez zavar, igaz, ne legyek telhetetlen :-)  :

failed to create drawable

Cz

Szerkesztve: 2020. 02. 22., szo – 12:54

wkhtml2pdf kiindulasnak?

Endi123 megoldását használom, de belefutottam egy jelenségbe: adott oldalon szerepel egy kép, amely nem közvetlen erőforrás hivatkozás, hanem egy képgeneráló URL, valami ilyesmi:

 <img src="http://XYZ.hu/artefact/file/download.php?file=8&amp;view=6&amp;time=1582494709" alt="Harvest-Bounty2-1024x1024.jpg" itemprop="contentURL" data-target="#configureblock" data-artefactid="8" data-blockid="27" title="Harvest-Bounty2-1024x1024.jpg">

A HTML állomány elmentett képén pedig csak az alt érték jelenik meg, nem tölti le a képet.

Mester, segíts :-)

Ha megvan az előnézet amit szeretnél, akkor a képet külön töltsd le, és koordináta alapján helyezd a képet a képre, feltéve ha ismerhető az x,y.
Pythonban a Pillow segíthet.

Szerk.:
^ ha nincs más lehetőség.

Biztos, hogy nem én gonoszoltam le!

A webkit másképp kezeli a képnek látszó hivatkozást és az egyéb src hivatkozást? Néhány ötlet:

  • Ha te írod a képgeneráló php kódot, akkor meg tudod változtatni úgy, hogy az src hivatkozást a böngésző kép kiterjesztésűnek lássa, a szerver pedig előállítja a kép neve alapján.
  • Az alt attr.-ról azt írják: "Omitting alt altogether indicates that the image is a key part of the content and no textual equivalent is available. Setting this attribute to an empty string (alt="") indicates that this image is not a key part of the content". Mindez nekem azt sejteti, hogy ha kihagyod az alt-ot, akkor a kép "key part"-á válik, és ez talán kényszeríti a böngészőt a betöltésre. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Img

Ha ezek nem jelentenek megoldást, akkor először is leellenőrízném hogy a webkit amúgy betölti az src képet ha hagyunk neki időt.

  • Nem-szerveren megteheted így: a python scriptben a "headless = False" legyen, és kommenteld ki a "self.close()" sort.
  • Szerveren például úgy tudod megtenni, hogy a python scriptben a WebKit2.LoadEvent.FINISHED után még vársz egy kicsit. Változtatások: GLib hozzáadása az importhoz:
    from gi.repository import Gtk, Gdk, GLib, WebKit2

    és lejjebb változik az on_load_changed(), és új az on_timeout() metódus:

        def on_load_changed(self, web_view, load_state):
            print("Load-state:", load_state)
            if load_state == WebKit2.LoadEvent.FINISHED and not hasattr(self, "timeout_count"):
                self.timeout_count = 4
                Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 1000, self.on_timeout)
        
        def on_timeout(self, *args):
            self.timeout_count -= 1
            if self.timeout_count > 0:
                print("Snapshot in ", self.timeout_count)
                return GLib.SOURCE_CONTINUE
            else:
                print("Taking snapshot ...")
                self.webview.get_snapshot(WebKit2.SnapshotRegion.FULL_DOCUMENT, snapshot_opts, cancellable=None, callback=self.on_snapshot_finish)
                return GLib.SOURCE_REMOVE

    ezek után a kimetet:

    Snapshot in 3
    Snapshot in 2
    Snapshot in 1
    Taking snapshot ...
    Snapshot ready

Ha az eddigiek nem jelentenek valamiféle megoldást, és ahogy írtad a webview settings beállításaival sem tudsz eredményt elérni, akkor javascript-ben lehetne a minden kép betöltődése eseményt elkapni (window.onload ?), és azt az eseményt pythonba továbbítani, de erről az irányról idő hiányában talán később.

Az alábbi módosítás sztem megoldja a problémát, lementi amit le lehet. Általánosságban a probléma hogy a mai weboldalak többségében js vezérli a betöltéseket (lazy loading, on-demand loading), ezért az "oldal betöltődése kész" esemény nehezen értelmezhető. Új resource betöltődése a LoadEvent.FINISHED esemény után! is történhet, szerencsére a webkit-et használó programban követhető az oldalhoz tartozó resource-ok betöltődési folyamata. Erre épül a következő módosítás (alapja az első hozzászólásom ), amely nem csinál képernyőmentést rögtön a LoadEvent.FINISHED után, ehelyett megvárja resource-ok betöltődését is.

Ahogy korábban most is hozzáadjuk a GLib-et az import-okhoz:

from gi.repository import Gtk, Gdk, GLib, WebKit2

A Browser osztályt egészben közlöm:

class Browser(base):
    def __init__(self, uri):
        super(Browser, self).__init__()
        
        self.webview = WebKit2.WebView.new_with_context(ctx)
        self.webview.connect("load-changed", self.on_load_changed)
        
        # Uj sorok
        self.timer_ms = 500
        self.resource_count_total = 0
        self.resources_loading_set = set()
        self.webview.connect("resource-load-started", self.on_resource_load_started)
        
        self.webview.load_uri(uri)
        self.add(self.webview)

        self.connect("destroy", Gtk.main_quit)
        self.set_size_request(render_width, 600) # set minimum size allowed

        self.set_title("Bongeszo - " + uri)
        self.show_all()
    
    def restart_timer(self):
        self.remove_timer()
        
        # Start timer only if the page is "loaded" and all resource load is finished
        if not self.webview.props.is_loading and len(self.resources_loading_set) == 0:
            self.timer_id = Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, self.timer_ms, self.on_timeout)
    
    def remove_timer(self):
        print("\t\t\t\t\t\t\t\t(resources loading: ", len(self.resources_loading_set), ", webview loading: ", self.webview.props.is_loading, ")", sep="")
        
        if hasattr(self, "timer_id"):
            GLib.source_remove(self.timer_id)
            del self.timer_id
            
    # WebView events
    
    def on_resource_load_started(self, web_view, resource, request):
        print("Resource load STARTED :", resource.get_uri())
        resource.connect("finished", self.on_resource_finished)
        if resource.get_response() is None:
            self.resources_loading_set.add(resource)
        self.remove_timer()
        self.resource_count_total += 1
        
    def on_resource_finished(self, resource):
        print("Resource load FINISHED:", resource.get_uri())
        self.resources_loading_set.discard(resource)
        self.restart_timer()
        
    def on_load_changed(self, web_view, load_state):
        print("\nLoad-state:", load_state, "\n")
        self.restart_timer()
    
    def on_timeout(self, *args):
        print("Resources loaded total:", self.resource_count_total)
        print("No load activity for", self.timer_ms, "ms, taking snapshot ...")
        self.webview.get_snapshot(WebKit2.SnapshotRegion.FULL_DOCUMENT, snapshot_opts, cancellable=None, callback=self.on_snapshot_finish)
        return GLib.SOURCE_REMOVE
    
    def on_snapshot_finish(self, web_view, result):
        print("Snapshot ready")
        cairo_surf = web_view.get_snapshot_finish(result)
        cairo_surf.write_to_png(dst_png_path)
        print("Snapshot saved to", dst_png_path)
        self.close()

A megértéséhez talán annyi elég, hogy ha a WebKit2.LoadEvent.FINISHED megvolt, és az utolsó resource is betöltődött, akkor elindít egy timer-t, amely self.timer_ms = 500 ideig vár. Ha a várakozás alatt új resource betöltés indul el, akkor a timer-t lelövi, később a betöltés befejezésekor újraindítja. Ha megszakítás nélkül lefut a timer, akkor menti a képernyőt és kilép.

Megjegyzés: a js lusta (kép)betöltést erősen használó oldalak, mint a youtube csak akkor töltenek be bizonyos képeket, ha lefelé görgeted az oldalt, sőt a görgetést figyelve akár bővítik is az oldalt. Ilyen esetek részben kezelhetőek azzal hogy a self.set_size_request(render_width, 600) sorban növeljük a 600-as height-et pár ezerre, vagy meg kell írni az szimulált (végig)görgetést.

Érdekesség, hogy a https://hup.hu cimlapjához a böngészőink 140 db resource-ot töltenek le, sok a google és a twitter. Itt a log vége:

Resource load STARTED : https://ton.twimg.com/tfw/assets/news_stroke_v1_78ce5b21fb24a7c7e528d22…
Resource load FINISHED: https://ton.twimg.com/tfw/assets/news_stroke_v1_78ce5b21fb24a7c7e528d22…
Resources loaded total: 140
No load activity for 500 ms, taking snapshot ...
Snapshot ready
Snapshot saved to hup.png

Egyre kifinomultabb a programod, de sajnos még mindig nem tölti le a PHP által generált képet.

 

Python executable: /usr/bin/python3
Python version:    3.6.9 (default, Nov  7 2019, 10:44:02)  [GCC 8.3.0]
Python venv:       not in venv
Python path:      
	/var/www/html/mahara/HUNGLE
	/usr/lib/python36.zip
	/usr/lib/python3.6
	/usr/lib/python3.6/lib-dynload
	/usr/local/lib/python3.6/dist-packages
	/usr/lib/python3/dist-packages
Module gi path:    /usr/lib/python3/dist-packages/gi/__init__.py

Load-state: <enum WEBKIT_LOAD_STARTED of type WebKit2.LoadEvent> 

								(resources loading: 0, webview loading: True)
Resource load STARTED : http://xyz.hu/HUNGLE/27.html
								(resources loading: 1, webview loading: True)

Load-state: <enum WEBKIT_LOAD_COMMITTED of type WebKit2.LoadEvent> 

								(resources loading: 1, webview loading: True)
Resource load STARTED : http://xyz.hu/HUNGLE/style.ccs
								(resources loading: 2, webview loading: True)
Resource load STARTED : http://xyz.hu/artefact/file/download.php?file=8&view=6&time=1582701544
								(resources loading: 3, webview loading: True)
Resource load FINISHED: http://xyz.hu/HUNGLE/style.ccs
								(resources loading: 2, webview loading: True)
Resource load FINISHED: http://xyz.hu/HUNGLE/27.html
								(resources loading: 1, webview loading: True)
Resource load FINISHED: http://xyz.hu/artefact/file/download.php?file=8&view=6&time=1582701544&login
								(resources loading: 0, webview loading: True)

Load-state: <enum WEBKIT_LOAD_FINISHED of type WebKit2.LoadEvent> 

								(resources loading: 0, webview loading: False)
Resources loaded total: 3
No load activity for 500 ms, taking snapshot ...
Snapshot ready
Snapshot saved to 27.png

Láthatóan felismeri az erőforrást, de nem kezd vele semmit.

A korábban megfogalmazott ötleteidet végig jártam, de ott semmi változást nem tapasztaltam. Elkerülő úton sikerült megpatkolnom a szervert, hogy állomány szinten elérhető legyen a kép, de jobb lenne, ha sikerülne a webkit2gtk-val megoldani a nagy problémát.

Hogyan tovább?

Resource load STARTED : http://xyz.hu/artefact/file/download.php?file=8&view=6&time=1582701544 Resource load FINISHED: http://xyz.hu/artefact/file/download.php?file=8&view=6&time=1582701544&login

Összevetve a 2 kimeneti sort, látható hogy történt egy redirect. A fejlesztői gépen a böngésződben be lehetsz jelentkezve az oldaladra, ezért ott nem kapod a redirect-et a &login -ra. A képletöltő headless böngészők resource lekérése viszont át lesz irányítva, az img tag src hivatkozására egy html-t kapnak vissza, amivel helyesen nem kezd semmit.

A betöltési hibák kiszűrésére egy nagyon hasznos, mindenképpen ajánlott kiegészítés legutóbbi hsz-emhez, csak az on_resource_finished() változik - kiírja a választ:

    def on_resource_finished(self, resource):
        if resource.get_response() is None:
            resp_mime = "None"
        else:
            resp_mime = "status_code: " + str(resource.get_response().get_status_code()) + ", mime_type: " + str(resource.get_response().get_mime_type())
        print("Resource load FINISHED:", resource.get_uri())
        print("              RESPONSE:", resp_mime)
        self.resources_loading_set.discard(resource)
        self.restart_timer()

Így mostmár a kimeneten az is látszik, hogy pl a css-re 404-et kapok (el van írva a kiterjesztés?), az img hivatkozásra pedig html-t:

Resource load FINISHED: http://xyz.hu/HUNGLE/style.ccs
              RESPONSE: status_code: 404, mime_type: text/html
								(resources loading: 1, webview loading: True)
Resource load FINISHED: http://xyz.hu/artefact/file/download.php?file=8&view=6&time=1582702325&login
              RESPONSE: status_code: 200, mime_type: text/html
								(resources loading: 0, webview loading: True)

Bocs, hogy telesírom ezt a fórumot a bajaimmal, de egyrészt ez megnyugtat :-), másrészt hátha akad kolléga, akinek lenne segítő gondolata. A fenti téma Google-keresése közben egyszerűsödött a kívánságom:

Van-e valami CLI megoldás a dinamikusan generált weboldalak letöltésére (esetleg WebKit2GTK python kóddal :-) )? Azt olvastam, hogy a wget, curl ugyan nem beszél javascript-tül, de nekem egy PHP kód ad vissza egy képet, azt kéne megvárnom, de nem biztatnak sok jóval. A legtöbb helyen a phantomjs-t ajánlják, amit elég régóta nem fejlesztenek és amikor kipróbáltam, hááát, nem volt valami szép a végeredmény, ráadásul hiányos volt a generált tartalom.

A Selenium felmerült, Firefox alá telepítettem a Selenium IDE-t, jópofa, csak pont nincs benne képernyőmentés.

Eddig a WebKit2GTK tűnik a legjobb megoldásnak, bár egyelőre ennek is vérzik a torka.

Valahogy nehéz elhinni, hogy tényleg ilyen rétegigényt fogalmaztam meg, senkinek nem volt szüksége arra, amire nekem. Vagy megoldják máshogy?

 

Üdv, Cz