company_banner

Как починить все самому, если баг-репорты игнорируются: отлаживаю wkhtmltopdf под Windows

    wkhtmltopdf — это один из самых мощных инструментов для генерации PDF. Он позволяет использовать в генерируемом документе все возможности HTML и CSS. «Под капотом» у него движок WebKit, так что результат почти в точности соответствует выводу «Print to PDF», встроенному в Chrome. Судя по вопросам на Stack Overflow, wkhtmltopdf используется для генерации карт, графиков, бухгалтерских отчётов, подарочных сертификатов, и практически любого другого контента, который в конечном счёте должен оказаться распечатанным на бумаге.



    Мой давний заказчик с помощью wkhtmltopdf генерирует PDF-инвойсы в своём веб-магазине. При печати в «шапке» инвойса должен отображаться чёрно-белый логотип, тогда как на сайте используется цветной. Очевидное решение — подменить изображение в CSS @media print { ... } Но тут обнаружилась проблема: если изображение не используется вне @media print, то оно не загружается и при печати (этот баг можно заметить и в окне Print Preview самого Chrome).

    Я зарепортил эту проблему, но за пару недель не дождался никакой реакции. Тогда я понял, что если мне мешает какая-то проблема, то и исправлять её мне надо самому — в истинном духе Open Source. В этой статье я покажу, как можно найти и исправить баг, если мейнтейнеры проекта не откликаются.

    Устройство wkhtmltopdf


    После беглого знакомства с содержимым репозитория wkhtmltopdf кажется, что он написан на C++ с использованием фреймворка Qt. На самом же деле, к wkhtmltopdf прилагается собственный форк Qt 4.8 с несколькими десятками изменений, внесённых специально для wkhtmltopdf. Это уже означает, что сборка wkhtmltopdf начинается со сборки всего Qt целиком — полугигабайта исходного кода на C++, большая часть которого не имеет отношения к WebKit и не используется в wkhtmltopdf.

    Но создатели wkhtmltopdf как будто бы специально искали, как усложнить сборку ещё сильнее. Сборка версий для Linux осуществляется внутри Docker-контейнера; версий для Windows и macOS — внутри виртуальной машины (Vagrant / VirtualBox). И то, и другое предусмотрено только для запуска на Linux-хосте; я же пользуюсь для работы Windows, и хотел собирать и отлаживать wkhtmltopdf именно под Windows. Компиляция Linux-версий в Docker под Windows, скорее всего, невозможна в принципе; но компиляция Windows-версии в VirtualBox, по идее, не должна зависеть от хост-платформы, верно? Не тут-то было…

    Как собрать wkhtmltopdf под Windows (первая горсть проблем)


    README.md утверждает, что сборка — это совсем просто:

    For building, just use the ./build vagrant command and it will bring up the VM, rsync the code into it, build dependent libraries via conan and compile Qt along with wkhtmltopdf, package it and copy the package into the output folder.

    Устанавливаем зависимости (VirtualBox, Vagrant, rsync), и пытаемся, как написано в readme, запустить ./build vagrant:

    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant
    usage: build vagrant [-h] [--clean] [--debug] [--version VER ITER] [--iteration RELITER] target src_dir
    build vagrant: error: the following arguments are required: target, src_dir
    

    Какой target следует указывать для сборки, readme даже не намекает; зато там упомянуто, что конфигурация для всех targets хранится в файле build.yml. Заглянув внутрь него, видим перечень всех возможных targets, и выбираем подходящий — он называется msvc2015-win64.

    Запускаем сборку
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Importing base box 'mcandre/windows-amd64'...
    ==> windows: Matching MAC address for NAT networking...
    ==> windows: Setting the name of the VM: vagrant_windows_1590526601203_40379
    ==> windows: Clearing any previously set network interfaces...
    ==> windows: Preparing network interfaces based on configuration...
        windows: Adapter 1: nat
    ==> windows: Forwarding ports...
        windows: 22 (guest) => 2222 (host) (adapter 1)
    ==> windows: Running 'pre-boot' VM customizations...
    ==> windows: Booting VM...
    ==> windows: Waiting for machine to boot. This may take a few minutes...
        windows: SSH address: 127.0.0.1:2222
        windows: SSH username: vagrant
        windows: SSH auth method: private key
    ==> windows: Machine booted and ready!
    ==> windows: Checking for guest additions in VM...
        windows: No guest additions were detected on the base box for this VM! Guest
        windows: additions are required for forwarded ports, shared folders, host only
        windows: networking, and more. If SSH fails on this machine, please install
        windows: the guest additions and repackage the box to continue.
        windows:
        windows: This is not an error message; everything may continue to work properly,
        windows: in which case you may ignore this message.
    ==> windows: Running provisioner: shell...
        windows: Running: inline script
        windows: vagrant@VAGRANT-IL06I9S C:\Users\vagrant>choco uninstall -y rsync
    [skipped]
        windows: vagrant@VAGRANT-IL06I9S C:\Users\vagrant>cd "C:/Program Files/Git"
        windows: vagrant@VAGRANT-IL06I9S C:\Program Files\Git>curl -fsSL https://downloads.sourceforge.net/msys2/rsync-3.1.3-1-x86_64.pkg.tar.xz | tar xJ
        windows: curl: (6) Could not resolve host: downloads.sourceforge.net
        windows: xz: (stdin): File format not recognized
        windows: tar: Child returned status 1
        windows: tar: Error is not recoverable: exiting now
    


    Как видно, в виртуальной машине не работает DNS.

    Интересно, к какому DNS-серверу она пытается обращаться?
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ cd vagrant/
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging/vagrant (0.12)
    $ vagrant ssh windows
    Microsoft Windows [Version 10.0.16299.15]
    (c) 2017 Microsoft Corporation. All rights reserved.
    
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>nslookup
    DNS request timed out.
        timeout was 2 seconds.
    Default Server:  UnKnown
    Address:  10.0.2.3
    
    > downloads.sourceforge.net.
    Server:  UnKnown
    Address:  10.0.2.3
    
    DNS request timed out.
        timeout was 2 seconds.
    DNS request timed out.
        timeout was 2 seconds.
    *** Request to UnKnown timed-out
    


    Ищем, что за проблемы могут быть в Vagrant / VirtualBox с DNS-сервером 10.0.2.3, и сразу же находим на Server Fault предложение включить в виртуальной машине опцию natdnshostresolver1.

    Добавляем в Vagrantfile посоветованную строчку:

                v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"]

    — и создаём виртуальную машину заново
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging/vagrant (0.12)
    $ cd ..
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Importing base box 'mcandre/windows-amd64'...
    [skipped]
        windows: vagrant@VAGRANT-IL06I9S C:\Program Files\Git>powershell Restart-Service sshd
    The source and destination cannot both be remote.
    rsync error: syntax or usage error (code 1) at main.c(1292) [Receiver=3.1.2]
    rsync --info=progress2 -a -e "ssh -F vagrant/.vagrant/windows_config" --delete --exclude .git C:\Users\tyomitch\Documents\wkhtmltopdf/ windows:/c/Users/vagrant/msvc2015-win64/src
    command failed: exit code 1
    


    Инициализация виртуальной машины прошла успешно, но попытка скопировать на неё репозиторий при помощи rsync столкнулась с проблемой: rsync считает двоеточие разделителем имени хоста и пути, так что путь
    C:\Users\tyomitch\Documents\wkhtmltopdf/ для rsync означает "\Users\tyomitch\Documents\wkhtmltopdf/ на хосте C". Таким образом, используемый локальный путь не должен быть абсолютным, а нам надо удалить из скрипта build лишние вызовы os.path.abspath — перед def outside_vm(): и внутри def rsync(flags, src, tgt):

    Теперь сборка проходит на один шаг дальше
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Machine already provisioned. Run `vagrant provision` or use the `--provision`
    ==> windows: flag to force provisioning. Provisioners marked to run always will still run.
        562,459,244 100%  410.31kB/s    0:22:18 (xfr#47167, to-chk=0/52290)
            141,615 100%  110.96kB/s    0:00:01 (xfr#58, to-chk=0/84)
    Auto detecting your dev setup to initialize the default profile (C:\Users\vagrant\msvc2015-win64\pkg\.conan\profiles\default)
    Found Visual Studio 14
    Default settings
            os=Windows
            os_build=Windows
            arch=x86_64
            arch_build=x86_64
            compiler=Visual Studio
            compiler.version=14
            build_type=Release
    [skipped]
    conanfile.txt: Generator json created conanbuildinfo.json
    conanfile.txt: Generator txt created conanbuildinfo.txt
    conanfile.txt: Generated conaninfo.txt
    conanfile.txt: Generated graphinfo
    WARN: Remotes registry file missing, creating default one in C:\Users\vagrant\msvc2015-win64\pkg\.conan\remotes.json
    '..' is not recognized as an internal or external command,
    operable program or batch file.
    ../src\qt\configure -opensource -confirm-license -fast -release -static -graphicssystem raster -webkit -exceptions [skipped] -D LIBJPEG_STATIC OPENSSL_LIBS="-llibssl -llibcrypto -lUser32 -lAdvapi32 -lGdi32 -lCrypt32"
    command failed: exit code 1
    ssh -F vagrant/.vagrant/windows_config windows -- python msvc2015-win64/pkg/build vagrant --version "0.12.6" "0.20200528.27.dev.f1ef81d" msvc2015-win64 ../src
    command failed: exit code 1
    


    Для командной строки Windows "../src\qt\configure" означает команду .. с ключом /src\qt\configure — и естественно, что такая команда приводит к ошибке. Это значит, что удалённые вызовы os.path.abspath были не совсем лишними, и внутри def inside_vm(): придётся дважды обернуть использование src_dir в вызов os.path.abspath — при запуске qt/configure в самом начале сборки, и при запуске qmake в самом конце. Пользуясь поводом, добавим в начало def inside_vm(): ещё и
            shell('powershell "Set-MpPreference -DisableRealtimeMonitoring $true"')
    — без этой команды все копируемые с хоста или генерируемые компилятором файлы проверяются Windows Defender, что привносит кошмарные тормоза.

    Повторяем попытку с исправленным скриптом build:

    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Machine already provisioned. Run `vagrant provision` or use the `--provision`
    ==> windows: flag to force provisioning. Provisioners marked to run always will still run.
                  0   0%    0.00kB/s    0:00:00 (xfr#0, ir-chk=1565/19221)Connection to 127.0.0.1 closed by remote host.
    
    rsync: connection unexpectedly closed (2084 bytes received so far) [sender]
    rsync error: error in rsync protocol data stream (code 12) at io.c(226) [sender=3.1.2]
    rsync --info=progress2 -a -e "ssh -F vagrant/.vagrant/windows_config" --delete --exclude .git ../wkhtmltopdf/ windows:/c/Users/vagrant/msvc2015-win64/src
    command failed: exit code 12
    

    Виртуальная машина внезапно выключилась во время синхронизации репозитория. От создания машины прошёл час — может, она нарочно выключается спустя час работы?

    Попробуем запустить сборку повторно
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Clearing any previously set forwarded ports...
    ==> windows: Clearing any previously set network interfaces...
    ==> windows: Preparing network interfaces based on configuration...
        windows: Adapter 1: nat
    ==> windows: Forwarding ports...
        windows: 22 (guest) => 2222 (host) (adapter 1)
    ==> windows: Running 'pre-boot' VM customizations...
    ==> windows: Booting VM...
    ==> windows: Waiting for machine to boot. This may take a few minutes...
        windows: SSH address: 127.0.0.1:2222
        windows: SSH username: vagrant
        windows: SSH auth method: private key
        windows: Warning: Connection aborted. Retrying...
    ==> windows: Machine booted and ready!
    ==> windows: Checking for guest additions in VM...
        windows: No guest additions were detected on the base box for this VM! Guest
        windows: additions are required for forwarded ports, shared folders, host only
        windows: networking, and more. If SSH fails on this machine, please install
        windows: the guest additions and repackage the box to continue.
        windows:
        windows: This is not an error message; everything may continue to work properly,
        windows: in which case you may ignore this message.
    ==> windows: Machine already provisioned. Run `vagrant provision` or use the `--provision`
    ==> windows: flag to force provisioning. Provisioners marked to run always will still run.
                  0   0%    0.00kB/s    0:00:00 (xfr#0, to-chk=0/52290)
             14,271  10%  259.86kB/s    0:00:00 (xfr#3, to-chk=0/88)
    Auto detecting your dev setup to initialize the default profile (C:\Users\vagrant\msvc2015-win64\pkg\.conan\profiles\default)
    [skipped]
    conanfile.txt: Generated graphinfo
    WARN: Remotes registry file missing, creating default one in C:\Users\vagrant\msvc2015-win64\pkg\.conan\remotes.json
    Preparing build tree...
    Setting accessibility to NO
    
    This is the Qt for Windows Open Source Edition.
    
    You have already accepted the terms of the license.
    [skipped]
    header (master) created for QtScriptTools
    headers.pri file created for QtScriptTools
    mkdir C:/Users/vagrant/msvc2015-win64/build/qt/src/tools
    mkdir C:/Users/vagrant/msvc2015-win64/build/qt/src/tools/uic
    Creating qmake...
    
    Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
            cl -c -Fo./  -W3 -nologo -O2    -I. -Igenerators -Igenerators\unix -Igenerators\win32 -Igenerators\mac -Igenerators\symbian -Igenerators\integrity  [skipped] -DQT_NO_QOBJECT -DQT_NO_GEOM_VARIANT -DQT_NO_DATASTREAM -DQT_NO_PCRE -DQT_BOOTSTRAPPED  -DQLIBRARYINFO_EPOCROOT -c -Yc -Fpqmake_pch.pch -TP qmake_pch.h
    qmake_pch.h
    [skipped]
    c:\users\vagrant\msvc2015-win64\src\qt\src\3rdparty\webkit\source\javascriptcore\runtime\PropertyMapHashTable.h(424): warning C4267: 'return': conversion from 'size_t' to 'unsigned int', possible loss of data (compiling source file c:\Users\vagrant\msvc2015-win64\src\qt\src\3rdparty\webkit\Source\JavaScriptCore\API\JSValueRef.cpp)
    Connection to 127.0.0.1 closed by remote host.
    ssh -F vagrant/.vagrant/windows_config windows -- python msvc2015-win64/pkg/build vagrant --version "0.12.6" "0.20200528.27.dev.f1ef81d" msvc2015-win64 ../src
    command failed: exit code 255
    


    Снова выключилась через час!

    Ищем, отчего может сама выключаться виртуальная машина в Vagrant / VirtualBox, и находим объяснение на Super User: Windows в виртуальной машине не активирована, но команда slmgr /rearm позволит пользоваться ей 30 дней без неожиданных отключений. По идее, эта команда должна выполняться один раз при инициализации виртуальной машины, так что стоит добавить в скрипт в cfg.vm.provision внутри Vagrantfile строчку start slmgr /rearm. После того, как эта команда выполнена на виртуальной машине, сборку можно запустить опять. Она перемалывает байты всю ночь, и к утру в папке targets хост-машины появляется готовый инсталлятор wkhtmltox-0.12.6-0.20200528.27.dev.f1ef81d.msvc2015-win64.exe

    Achievement unlocked: wkhtmltopdf собран под Windows! Но нам ведь нужен не инсталлятор, а отладочная версия для поиска бага?

    Как собрать под Windows отладочную версию (ещё горсть проблем)


    Мы помним, что у скрипта build есть параметр --debug, хоть он и не упомянут в README.md.

    Попробуем
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf --debug
    Bringing machine 'windows' up with 'virtualbox' provider...
    [skipped]
    conanfile.txt: Generator json created conanbuildinfo.json
    conanfile.txt: Generator txt created conanbuildinfo.txt
    conanfile.txt: Generated conaninfo.txt
    conanfile.txt: Generated graphinfo
    WARN: Remotes registry file missing, creating default one in C:\Users\vagrant\msvc2015-win64\pkg\.conan\remotes.json
    
    Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
            cd src\tools\bootstrap\ && "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\nmake.exe" -f Makefile
    
    Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
            "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\nmake.exe" -f Makefile.Release
    
    Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    NMAKE : fatal error U1073: don't know how to make 'c:\Users\vagrant\msvc2015-win64\pkg\libs\zlib\1.2.11\conan\stable\package\63da998e3642b50bee33f4449826b2d623661505\include\zlib.h'
    Stop.
    NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\nmake.exe"' : return code '0x2'
    Stop.
    NMAKE : fatal error U1077: 'cd' : return code '0x2'
    Stop.
    nmake
    command failed: exit code 2
    ssh -F vagrant/.vagrant/windows_config windows -- python msvc2015-win64/pkg/build vagrant --version "0.12.6" "0.20200528.27.dev.f1ef81d" --debug msvc2015-win64 ../src
    command failed: exit code 1
    


    Что за напасть: zlib.h после успешной релизной сборки куда-то потерялся? Похоже, что отладочная сборка в той же виртуальной машине, где уже выполнялась релизная сборка, просто не поддерживается. Не беда: релизная версия нам всё равно незачем, так что запустим сборку начисто.

    Запуск сборки
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf --debug --clean
    ==> windows: Forcing shutdown of VM...
    ==> windows: Destroying VM and associated drives...
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Importing base box 'mcandre/windows-amd64'...
    [skipped]
        windows: vagrant@VAGRANT-IL06I9S C:\Program Files\Git>powershell Restart-Service sshd
        562,459,244 100%  440.60kB/s    0:20:46 (xfr#47167, to-chk=0/52290)
            141,748 100%   89.37kB/s    0:00:01 (xfr#62, to-chk=0/88)
    Auto detecting your dev setup to initialize the default profile (C:\Users\vagrant\msvc2015-win64\pkg\.conan\profiles\default)
    Found Visual Studio 14
    Default settings
            os=Windows
            os_build=Windows
            arch=x86_64
            arch_build=x86_64
            compiler=Visual Studio
            compiler.version=14
            build_type=Release
    *** You can change them in C:\Users\vagrant\msvc2015-win64\pkg\.conan\profiles\default ***
    *** Or override with -s compiler='other' -s ...s***
    
    
    Configuration:
    [settings]
    arch=x86_64
    arch_build=x86_64
    build_type=Debug
    compiler=Visual Studio
    compiler.runtime=MDd
    compiler.version=14
    os=Windows
    os_build=Windows
    [options]
    [build_requires]
    [env]
    
    zlib/1.2.11@conan/stable: Not found in local cache, looking in remotes...
    zlib/1.2.11@conan/stable: Trying with 'conan-center'...
    [skipped]
            "C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\nmake.exe" -f Makefile.Debug
    
    Microsoft (R) Program Maintenance Utility Version 14.00.24210.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
            C:\Users\vagrant\msvc2015-win64\build\qt\bin\moc.exe -DQT_THREAD_SUPPORT -DUNICODE -DWIN32 -DQT_BUILD_CORE_LIB -DQT_NO_USING_NAMESPACE -DQT_ASCII_CAST_WARNINGS [skipped] -I"." -I"..\..\mkspecs\win32-msvc2015" -D_MSC_VER=1900 -DWIN32 c:\Users\vagrant\msvc2015-win64\src\qt\src\corelib\animation\qabstractanimation.h -o tmp\moc\debug_static\moc_qabstractanimation.cpp
    NMAKE : fatal error U1077: 'C:\Users\vagrant\msvc2015-win64\build\qt\bin\moc.exe' : return code '0xc0000135'
    Stop.
    NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\nmake.exe"' : return code '0x2'
    Stop.
    NMAKE : fatal error U1077: '""C:\Program' : return code '0x2'
    Stop.
    NMAKE : fatal error U1077: 'cd' : return code '0x2'
    Stop.
    NMAKE : fatal error U1077: '""C:\Program' : return code '0x2'
    Stop.
    nmake
    command failed: exit code 2
    ssh -F vagrant/.vagrant/windows_config windows -- python msvc2015-win64/pkg/build vagrant --version "0.12.6" "0.20200528.27.dev.f1ef81d" --clean --debug msvc2015-win64 ../src
    command failed: exit code 1
    


    Код ошибки 0xc0000135 — это STATUS_DLL_NOT_FOUND.

    Интересно, в какой DLL дело?
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ cd vagrant/
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging/vagrant (0.12)
    $ vagrant ssh windows
    Microsoft Windows [Version 10.0.16299.15]
    (c) 2017 Microsoft Corporation. All rights reserved.
    
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\dumpbin.exe" /DEPENDENTS C:\Users\vagrant\msvc2015-win64\build\qt\bin\moc.exe
    Microsoft (R) COFF/PE Dumper Version 14.00.24210.0
    Copyright (C) Microsoft Corporation.  All rights reserved.
    
    
    Dump of file C:\Users\vagrant\msvc2015-win64\build\qt\bin\moc.exe
    
    File Type: EXECUTABLE IMAGE
    
      Image has the following dependencies:
    
        USER32.dll
        KERNEL32.dll
        VCRUNTIME140D.dll
        ucrtbased.dll
    
      Summary
    
            1000 .00cfg
            2000 .data
            1000 .gfids
            2000 .idata
           13000 .pdata
           B6000 .rdata
            2000 .reloc
            1000 .rsrc
          137000 .text
            1000 .tls
    
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>dir \Windows\System32\vcruntime140*
     Volume in drive C is Windows 10
     Volume Serial Number is A0AB-6559
    
     Directory of C:\Windows\System32
    
    06/09/2016  10:53 PM            87,888 vcruntime140.dll
    06/09/2016  10:53 PM           131,920 vcruntime140d.dll
                   2 File(s)        219,808 bytes
    
                   0 Dir(s)  15,111,213,056 bytes free  
    
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>dir \Windows\System32\ucrtbase*
     Volume in drive C is Windows 10
     Volume Serial Number is A0AB-6559
    
     Directory of C:\Windows\System32
    
    09/29/2017  02:41 PM         1,003,104 ucrtbase.dll
    09/29/2017  02:41 PM           479,912 ucrtbase_enclave.dll
                   2 File(s)      1,483,016 bytes
    
                   0 Dir(s)  15,111,176,192 bytes free
    

    Каким-то образом при инициализации виртуальной машины установилась отладочная версия vcruntime140, но не установилась отладочная версия ucrtbase.

    А в принципе она на виртуальной машине есть, интересно?
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>dir /s \ucrtbased.dll
     Volume in drive C is Windows 10
     Volume Serial Number is A0AB-6559
    
     Directory of C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.UniversalCRT.Debug\10.0.10240.0\Redist\Debug\arm
    
    07/09/2015  08:58 PM         1,352,200 ucrtbased.dll
                   1 File(s)      1,352,200 bytes
    
     Directory of C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.UniversalCRT.Debug\10.0.10240.0\Redist\Debug\arm64
    
    07/09/2015  10:07 PM         1,803,272 ucrtbased.dll
                   1 File(s)      1,803,272 bytes
    
     Directory of C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.UniversalCRT.Debug\10.0.10240.0\Redist\Debug\x64
    
    07/09/2015  10:26 PM         1,808,576 ucrtbased.dll
                   1 File(s)      1,808,576 bytes
    
     Directory of C:\Program Files (x86)\Microsoft SDKs\Windows Kits\10\ExtensionSDKs\Microsoft.UniversalCRT.Debug\10.0.10240.0\Redist\Debug\x86
    
    07/09/2015  10:33 PM         1,514,176 ucrtbased.dll
                   1 File(s)      1,514,176 bytes
    
     Directory of C:\Program Files (x86)\Windows Kits\10\bin\arm\ucrt
    
    07/09/2015  08:59 PM         1,352,200 ucrtbased.dll
                   1 File(s)      1,352,200 bytes
    
    Directory of C:\Program Files (x86)\Windows Kits\10\bin\arm64\ucrt
    
    07/09/2015  10:03 PM         1,803,272 ucrtbased.dll
                   1 File(s)      1,803,272 bytes
    
    Directory of C:\Program Files (x86)\Windows Kits\10\bin\x64\ucrt
    
    07/09/2015  10:26 PM         1,808,576 ucrtbased.dll
                   1 File(s)      1,808,576 bytes
    
    Directory of C:\Program Files (x86)\Windows Kits\10\bin\x86\ucrt
    
    07/09/2015  10:31 PM         1,514,176 ucrtbased.dll
                   1 File(s)      1,514,176 bytes
    
         Total Files Listed:
                   8 File(s)     12,956,448 bytes
                   0 Dir(s)  15,111,176,192 bytes free
    


    Есть, и даже в восьми копиях, для четырёх разных архитектур — да только не там, где нужно!

    Значит, чтобы для Windows можно было собрать отладочную версию, в конец скрипта в cfg.vm.provision внутри Vagrantfile нужно добавить строчку
    copy "C:\\Program Files (x86)\\Windows Kits\\10\\bin\\x64\\ucrt\\ucrtbased.dll" C:\\Windows\\System32
    

    После того, как эта команда выполнена на виртуальной машине, отладочную сборку можно запустить опять.

    Запуск отладочной сборки
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging/vagrant (0.12)
    $ cd ..
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf --debug
    Bringing machine 'windows' up with 'virtualbox' provider...
    ==> windows: Machine already provisioned. Run `vagrant provision` or use the `--provision`
    ==> windows: flag to force provisioning. Provisioners marked to run always will still run.
    [skipped]
    compiling debug\moc_multipageloader_p.cpp debug\moc_converter_p.cpp debug\moc_pdfconverter_p.cpp debug\moc_imageconverter_p.cpp debug\moc_pdf_c_bindings_p.cpp debug\moc_image_c_bindings_p.cpp debug\moc_converter.cpp debug\moc_multipageloader.cpp debug\moc_utilities.cpp debug\moc_pdfconverter.cpp debug\moc_imageconverter.cpp debug\qrc_wkhtmltopdf.cpp
    moc_multipageloader_p.cpp
    moc_converter_p.cpp
    moc_pdfconverter_p.cpp
    moc_imageconverter_p.cpp
    moc_pdf_c_bindings_p.cpp
    moc_image_c_bindings_p.cpp
    moc_converter.cpp
    moc_multipageloader.cpp
    moc_utilities.cpp
    moc_pdfconverter.cpp
    moc_imageconverter.cpp
    qrc_wkhtmltopdf.cpp
    Generating Code...
    linking ..\..\bin\wkhtmltox.dll
    LINK : fatal error LNK1104: cannot open file 'libpng.lib'
    NMAKE : fatal error U1077: 'echo' : return code '0x450'
    Stop.
    NMAKE : fatal error U1077: '"C:\Program Files (x86)\Microsoft Visual Studio 14.0\VC\BIN\amd64\nmake.exe"' : return code '0x2'
    Stop.
    NMAKE : fatal error U1077: 'cd' : return code '0x2'
    Stop.
    nmake
    command failed: exit code 2
    ssh -F vagrant/.vagrant/windows_config windows -- python msvc2015-win64/pkg/build vagrant --version "0.12.6" "0.20200528.27.dev.f1ef81d" --debug msvc2015-win64 ../src
    command failed: exit code 1
    


    Ещё одна библиотека при отладочной сборке потерялась!

    Попытаемся её найти
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ cd vagrant/
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging/vagrant (0.12)
    $ vagrant ssh windows
    Microsoft Windows [Version 10.0.16299.15]
    (c) 2017 Microsoft Corporation. All rights reserved.
    
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>dir /s libpng.lib
     Volume in drive C is Windows 10
     Volume Serial Number is A0AB-6559
    File Not Found
    
    vagrant@VAGRANT-IL06I9S C:\Users\vagrant>dir /s libpng*.lib
     Volume in drive C is Windows 10
     Volume Serial Number is A0AB-6559
    
     Directory of C:\Users\vagrant\msvc2015-win64\pkg\libs\libpng\1.6.37\_\_\package\b17b520b4b55729a7391c6b2d20631fec4cf1564\lib
    
    05/30/2020  07:52 PM         1,002,396 libpng16d.lib
                   1 File(s)      1,002,396 bytes
    
         Total Files Listed:
                   1 File(s)      1,002,396 bytes
                   0 Dir(s)  12,720,320,512 bytes free
    


    Видим причину ошибки: отладочная версия библиотеки собралась с именем libpng16d.lib, но make-файл для wkhtmltox.dll всё равно ссылается на libpng.lib. Проблема, по-видимому, где-то внутри Qt, потому что сам wkhtmltopdf нигде явно не ссылается на libpng. Решить эту проблему проще всего добавлением костыля:

        if debug:
                shell('cp ../pkg/libs/libpng/1.6.37/_/_/package/b17b520b4b55729a7391c6b2d20631fec4cf1564/lib/libpng16d.lib ../pkg/libs/libpng/1.6.37/_/_/package/b17b520b4b55729a7391c6b2d20631fec4cf1564/lib/libpng.lib')

    — в скрипт build внутрь def inside_vm(): после вызова conan, который и собирает libpng.

    Возобновив сборку с этим костылём, ловим в том же месте вторую точно такую же ошибку «LNK1104: cannot open file 'libssl.lib'». libssl подключается к сборке wkhtmltopdf явно — внутри def prepare_build(config, target, build_dir, src_dir): в скрипте vagrant/windows.py; но эта процедура не получает значение параметра --debug и поэтому не может определить, надо ли добавлять «d» к названиям подключаемых библиотек. Перекраивать интерфейс сборки ради отладки под Windows как-то неловко, так что просто добавим к нашему костылю ещё пару строчек.

                shell('cp ../pkg/libs/openssl/1.1.1g/_/_/package/c32596dcd26b8c708dc3d19cb73738d2b48f12a8/lib/libssld.lib ../pkg/libs/openssl/1.1.1g/_/_/package/c32596dcd26b8c708dc3d19cb73738d2b48f12a8/lib/libssl.lib')
                shell('cp ../pkg/libs/openssl/1.1.1g/_/_/package/c32596dcd26b8c708dc3d19cb73738d2b48f12a8/lib/libcryptod.lib ../pkg/libs/openssl/1.1.1g/_/_/package/c32596dcd26b8c708dc3d19cb73738d2b48f12a8/lib/libcrypto.lib')
    

    Теперь отладочная версия wkhtmltopdf успешно собирается:

    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf --debug
    Bringing machine 'windows' up with 'virtualbox' provider...
    [skipped]
    qrc_wkhtmltopdf.cpp
    Generating Code...
    linking ..\..\bin\wkhtmltoimage.exe
       Creating library ..\..\bin\wkhtmltoimage.lib and object ..\..\bin\wkhtmltoimage.exp
            mt.exe -nologo -manifest "debug\wkhtmltoimage.intermediate.manifest" -outputresource:..\..\bin\wkhtmltoimage.exe;1
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $
    

    Как я отлаживал wkhtmltopdf и нашёл тот самый баг


    Для начала неплохо бы вытащить собранные бинарники из виртуальной машины:

    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ scp -F vagrant/.vagrant/windows_config windows:msvc2015-win64/build/app/bin/*  targets
    wkhtmltoimage.exe                             100%  115MB  15.8MB/s   00:07
    wkhtmltoimage.exp                             100%   77KB   2.1MB/s   00:00
    wkhtmltoimage.ilk                             100%  327MB  10.4MB/s   00:31
    wkhtmltoimage.lib                             100%  126KB   1.7MB/s   00:00
    wkhtmltoimage.pdb                             100%  234MB   8.6MB/s   00:27
    wkhtmltopdf.exe                               100%  116MB  10.0MB/s   00:11
    wkhtmltopdf.exp                               100%   77KB   2.4MB/s   00:00
    wkhtmltopdf.ilk                               100%  327MB  11.7MB/s   00:27
    wkhtmltopdf.lib                               100%  125KB   4.9MB/s   00:00
    wkhtmltopdf.pdb                               100%  234MB  10.7MB/s   00:21
    wkhtmltox.dll                                 100%  115MB  12.0MB/s   00:09
    wkhtmltox.exp                                 100%   77KB   2.1MB/s   00:00
    wkhtmltox.ilk                                 100%  326MB  11.4MB/s   00:28
    wkhtmltox.lib                                 100%  124KB   5.2MB/s   00:00
    wkhtmltox.pdb                                 100%  233MB  11.2MB/s   00:20
    


    Проверяем, что зарепорченный баг воспроизводится:

    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ echo '<style>@media print { body { background-image: url(https://habr.com/images/habr_ru.png) } }</style>' | targets/wkhtmltopdf --print-media-type - output.pdf
    Loading pages (1/6)
    Counting pages (2/6)
    Warning: Received createRequest signal on a disposed ResourceObject's NetworkAccessManager. This might be an indication of an iframe taking too long to load.
    Resolving links (4/6)
    Loading headers and footers (5/6)
    Printing pages (6/6)
    Done
    Error: Failed to load about:blank, with network status code 301 and http status code 0 - Protocol "about" is unknown
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ echo '<style>.force-load { display: none; background-image: url(https://habr.com/images/habr_ru.png) } @media print { body { background-image: url(https://habr.com/images/habr_ru.png) } }</style><div class="force-load" />' | targets/wkhtmltopdf --print-media-type - output.pdf
    Loading pages (1/6)
    Counting pages (2/6)
    Resolving links (4/6)
    Loading headers and footers (5/6)
    Printing pages (6/6)
    Done
    LEAK: 1 CachedResource
    


    По сравнению с релизной версией на сервере с веб-магазином моего заказчика, добавились два неожиданных сообщения: «Error: Failed to load about:blank» в первом случае, когда изображение не загружается, и «LEAK: 1 CachedResource» во втором случае, когда всё работает как надо. Логично предположить, что недозагрузка изображения как-то связана с попыткой загрузки about:blank. Этот URL в коде wkhtmltopdf упоминается дважды, и оба раза — внутри MyNetworkAccessManager::createRequest в файле multipageloader.cc. Для проверки нашей догадки попробуем заменить эти два URL на about:foo и about:bar соответственно, и пересобрать отладочную версию:

    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ echo '<style>@media print { body { background-image: url(https://habr.com/images/habr_ru.png) } }</style>' | targets/wkhtmltopdf --print-media-type - output.pdf
    Loading pages (1/6)
    Counting pages (2/6)
    Warning: Received createRequest signal on a disposed ResourceObject's NetworkAccessManager. This might be an indication of an iframe taking too long to load.
    Resolving links (4/6)
    Loading headers and footers (5/6)
    Printing pages (6/6)
    Done
    Error: Failed to load about:foo, with network status code 301 and http status code 0 - Protocol "about" is unknown
    

    Итак, причина недозагрузки выяснилась: MyNetworkAccessManager::dispose вызывается до того, как request на загрузку изображения создаётся и передаётся этому AccessManager-у. Теперь интересно узнать, кто так некстати вызывает dispose() и почему. Для этого добавим внутрь dispose() интринсик __debugbreak(), и запустим wkhtmltopdf.exe под отладчиком Visual Studio: File → Open → Project/Solution → wkhtmltopdf.exe, Project → Properties → Arguments → --print-media-type input.html output.pdf, Debug → Start Debugging. Как только выполнение доходит до __debugbreak(), то всплывает окно выбора «Find Source: multipageloader.cc»; после того, как выбран верный путь к файлу, отладчиком можно пользоваться как для обычного C++-проекта.

    image

    Самое интересное, конечно, в Call Stack: мы видим, что dispose() вызывается из ResourceObject::loadDone с красноречивым комментарием «Ensure no more loading goes..» git blame обнаруживает, что этот вызов добавлен в коммите «Try to ensure no more web requests can be made by finished resource objects (like when a JS script is trying to reload, etc.)»

    Возможно, если откатить этот коммит, то изображение успешно загрузится?
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ cd ../wkhtmltopdf
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/wkhtmltopdf (master)
    $ git stash
    Saved working directory and index state WIP on master: f1ef81d add downloads for Ubuntu 20.04
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/wkhtmltopdf (master)
    $ git revert 69a8cce
    Auto-merging src/lib/multipageloader.cc
    [master 3692d59] Revert "Try to ensure no more web requests can be made by finished resource objects (like when a JS script is trying to reload, etc.)"
     1 file changed, 7 deletions(-)
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/wkhtmltopdf (master)
    $ git stash pop
    Auto-merging src/lib/multipageloader.cc
    On branch master
    Your branch is ahead of 'origin/master' by 1 commit.
      (use "git push" to publish your local commits)
    
    Changes not staged for commit:
      (use "git add <file>..." to update what will be committed)
      (use "git restore <file>..." to discard changes in working directory)
            modified:   src/lib/multipageloader.cc
    
    no changes added to commit (use "git add" and/or "git commit -a")
    Dropped refs/stash@{0} (1d9908bbeeadcc5127dae765a39969ac353345d8)
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/wkhtmltopdf (master)
    $ cd ../packaging
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ ./build vagrant msvc2015-win64 ../wkhtmltopdf --debug
    Bringing machine 'windows' up with 'virtualbox' provider...
    [skipped]
       Creating library ..\..\bin\wkhtmltoimage.lib and object ..\..\bin\wkhtmltoimage.exp
            mt.exe -nologo -manifest "debug\wkhtmltoimage.intermediate.manifest" -outputresource:..\..\bin\wkhtmltoimage.exe;1
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ scp -F vagrant/.vagrant/windows_config windows:msvc2015-win64/build/app/bin/* target
    wkhtmltoimage.exe                             100%  115MB  14.7MB/s   00:07
    [skipped]
    wkhtmltox.pdb                                 100%  233MB  12.3MB/s   00:18
    
    tyomitch@DESKTOP-9VOKU6U MINGW64 ~/Documents/packaging (0.12)
    $ echo '<style>@media print { body { background-image: url(https://habr.com/images/habr_ru.png) } }</style>' | targets/wkhtmltopdf --print-media-type - output.pdf
    Loading pages (1/6)
    Counting pages (2/6)
    Resolving links (4/6)
    Loading headers and footers (5/6)
    Printing pages (6/6)
    Done


    Предупреждений про «disposed ResourceObject» и про попытку загрузить about:foo больше нет; но изображение в PDF-файле так и не появилось. Хмм, значит тот сомнительный коммит не был причиной проблемы, хотя и повлиял на её проявление.

    Дальнейшая стратегия отладки — определить, почему при использовании <div class="force-load" /> создаётся request для загрузки изображения, и почему без этого request не создаётся. Ставим breakpoint на MyNetworkAccessManager::createRequest и смотрим, откуда она вызывается.

    Call Stack
    wkhtmltopdf.exe!wkhtmltopdf::MyNetworkAccessManager::createRequest(QNetworkAccessManager::Operation op, const QNetworkRequest & req, QIODevice * outgoingData) Line 77
    wkhtmltopdf.exe!QNetworkAccessManager::get(const QNetworkRequest & request) Line 598
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandler::sendNetworkRequest(QNetworkAccessManager * manager, const WebCore::ResourceRequest & request) Line 626
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandler::start() Line 665
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandlerCallQueue::flush() Line 195
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandlerCallQueue::push(void(WebCore::QNetworkReplyHandler::*)() method) Line 165
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandler::QNetworkReplyHandler(WebCore::ResourceHandle * handle, WebCore::QNetworkReplyHandler::LoadType loadType, bool deferred) Line 401
    wkhtmltopdf.exe!WebCore::ResourceHandle::start(WebCore::NetworkingContext * context) Line 100
    wkhtmltopdf.exe!WebCore::ResourceHandle::create(WebCore::NetworkingContext * context, const WebCore::ResourceRequest & request, WebCore::ResourceHandleClient * client, bool defersLoading, bool shouldContentSniff) Line 71
    wkhtmltopdf.exe!WebCore::ResourceLoader::start() Line 164
    wkhtmltopdf.exe!WebCore::ResourceLoadScheduler::servePendingRequests(WebCore::ResourceLoadScheduler::HostInformation * host, WebCore::ResourceLoadPriority minimumPriority) Line 201
    wkhtmltopdf.exe!WebCore::ResourceLoadScheduler::scheduleLoad(WebCore::ResourceLoader * resourceLoader, WebCore::ResourceLoadPriority priority) Line 124
    wkhtmltopdf.exe!WebCore::ResourceLoadScheduler::scheduleSubresourceLoad(WebCore::Frame * frame, WebCore::SubresourceLoaderClient * client, const WebCore::ResourceRequest & request, WebCore::ResourceLoadPriority priority, WebCore::SecurityCheckPolicy securityCheck, bool sendResourceLoadCallbacks, bool shouldContentSniff, const WTF::String & optionalOutgoingReferrer) Line 92
    wkhtmltopdf.exe!WebCore::CachedResourceRequest::load(WebCore::CachedResourceLoader * cachedResourceLoader, WebCore::CachedResource * resource, bool incremental, WebCore::SecurityCheckPolicy securityCheck, bool sendResourceLoadCallbacks) Line 124
    wkhtmltopdf.exe!WebCore::CachedResourceLoader::load(WebCore::CachedResource * resource, bool incremental, WebCore::SecurityCheckPolicy securityCheck, bool sendResourceLoadCallbacks) Line 541
    wkhtmltopdf.exe!WebCore::CachedResource::load(WebCore::CachedResourceLoader * cachedResourceLoader, bool incremental, WebCore::SecurityCheckPolicy securityCheck, bool sendResourceLoadCallbacks) Line 134
    wkhtmltopdf.exe!WebCore::CachedImage::load(WebCore::CachedResourceLoader * cachedResourceLoader) Line 88
    wkhtmltopdf.exe!WebCore::CachedResourceLoader::loadResource(WebCore::CachedResource::Type type, const WebCore::KURL & url, const WTF::String & charset, WebCore::ResourceLoadPriority priority) Line 395
    wkhtmltopdf.exe!WebCore::CachedResourceLoader::requestResource(WebCore::CachedResource::Type type, const WTF::String & resourceURL, const WTF::String & charset, WebCore::ResourceLoadPriority priority, bool forPreload) Line 328
    wkhtmltopdf.exe!WebCore::CachedResourceLoader::requestImage(const WTF::String & url) Line 137
    wkhtmltopdf.exe!WebCore::CSSImageValue::cachedImage(WebCore::CachedResourceLoader * loader, const WTF::String & url) Line 74
    wkhtmltopdf.exe!WebCore::CSSImageValue::cachedImage(WebCore::CachedResourceLoader * loader) Line 64
    wkhtmltopdf.exe!WebCore::CSSStyleSelector::loadPendingImages() Line 7068
    wkhtmltopdf.exe!WebCore::CSSStyleSelector::styleForElement(WebCore::Element * e, WebCore::RenderStyle * defaultParent, bool allowSharing, bool resolveForRootDefault, bool matchVisitedPseudoClass) Line 1507
    wkhtmltopdf.exe!WebCore::Node::styleForRenderer() Line 1624
    wkhtmltopdf.exe!WebCore::NodeRendererFactory::createRendererAndStyle() Line 1553
    wkhtmltopdf.exe!WebCore::NodeRendererFactory::createRendererIfNeeded() Line 1592
    wkhtmltopdf.exe!WebCore::Node::createRendererIfNeeded() Line 1614
    wkhtmltopdf.exe!WebCore::Element::attach() Line 1000
    wkhtmltopdf.exe!WebCore::HTMLConstructionSite::attach<WebCore::Element>(WebCore::ContainerNode * rawParent, WTF::PassRefPtr<WebCore::Element> prpChild) Line 108
    wkhtmltopdf.exe!WebCore::HTMLConstructionSite::attachToCurrent(WTF::PassRefPtr<WebCore::Element> child) Line 259
    wkhtmltopdf.exe!WebCore::HTMLConstructionSite::insertHTMLElement(WebCore::AtomicHTMLToken & token) Line 289
    wkhtmltopdf.exe!WebCore::HTMLTreeBuilder::processStartTagForInBody(WebCore::AtomicHTMLToken & token) Line 796
    wkhtmltopdf.exe!WebCore::HTMLTreeBuilder::processStartTag(WebCore::AtomicHTMLToken & token) Line 1229
    wkhtmltopdf.exe!WebCore::HTMLTreeBuilder::processToken(WebCore::AtomicHTMLToken & token) Line 480
    wkhtmltopdf.exe!WebCore::HTMLTreeBuilder::constructTreeFromAtomicToken(WebCore::AtomicHTMLToken & token) Line 465
    wkhtmltopdf.exe!WebCore::HTMLTreeBuilder::constructTreeFromToken(WebCore::HTMLToken & rawToken) Line 452
    wkhtmltopdf.exe!WebCore::HTMLDocumentParser::pumpTokenizer(WebCore::HTMLDocumentParser::SynchronousMode mode) Line 277
    wkhtmltopdf.exe!WebCore::HTMLDocumentParser::pumpTokenizerIfPossible(WebCore::HTMLDocumentParser::SynchronousMode mode) Line 176
    wkhtmltopdf.exe!WebCore::HTMLDocumentParser::append(const WebCore::SegmentedString & source) Line 369
    wkhtmltopdf.exe!WebCore::DecodedDataDocumentParser::appendBytes(WebCore::DocumentWriter * writer, const char * data, int length, bool shouldFlush) Line 54
    wkhtmltopdf.exe!WebCore::DocumentWriter::addData(const char * str, int len, bool flush) Line 209
    wkhtmltopdf.exe!WebCore::DocumentWriter::endIfNotLoadingMainResource() Line 229
    wkhtmltopdf.exe!WebCore::DocumentWriter::end() Line 215
    wkhtmltopdf.exe!WebCore::DocumentLoader::finishedLoading() Line 290
    wkhtmltopdf.exe!WebCore::FrameLoader::finishedLoading() Line 2294
    wkhtmltopdf.exe!WebCore::MainResourceLoader::didFinishLoading(double finishTime) Line 485
    wkhtmltopdf.exe!WebCore::ResourceLoader::didFinishLoading(WebCore::ResourceHandle * __formal, double finishTime) Line 440
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandler::finish() Line 455
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandlerCallQueue::flush() Line 195
    wkhtmltopdf.exe!WebCore::QNetworkReplyHandlerCallQueue::push(void(WebCore::QNetworkReplyHandler::*)() method) Line 165
    wkhtmltopdf.exe!WebCore::QNetworkReplyWrapper::didReceiveFinished() Line 350
    wkhtmltopdf.exe!WebCore::QNetworkReplyWrapper::qt_static_metacall(QObject * _o, QMetaObject::Call _c, int _id, void * * _a) Line 56
    wkhtmltopdf.exe!QMetaObject::activate(QObject * sender, const QMetaObject * m, int local_signal_index, void * * argv) Line 3567
    wkhtmltopdf.exe!QNetworkReply::finished() Line 166
    wkhtmltopdf.exe!QNetworkReply::qt_static_metacall(QObject * _o, QMetaObject::Call _c, int _id, void * * _a) Line 106
    wkhtmltopdf.exe!QMetaCallEvent::placeMetaCall(QObject * object) Line 525
    wkhtmltopdf.exe!QObject::event(QEvent * e) Line 1222
    wkhtmltopdf.exe!QApplicationPrivate::notify_helper(QObject * receiver, QEvent * e) Line 4565
    wkhtmltopdf.exe!QApplication::notify(QObject * receiver, QEvent * e) Line 3947
    wkhtmltopdf.exe!QCoreApplication::notifyInternal(QObject * receiver, QEvent * event) Line 955
    wkhtmltopdf.exe!QCoreApplication::sendEvent(QObject * receiver, QEvent * event) Line 231
    wkhtmltopdf.exe!QCoreApplicationPrivate::sendPostedEvents(QObject * receiver, int event_type, QThreadData * data) Line 1579
    wkhtmltopdf.exe!qt_internal_proc(HWND__ * hwnd, unsigned int message, unsigned __int64 wp, __int64 lp) Line 498
    

    Логично предположить, что разгадка где-то внутри CSSStyleSelector::styleForElement. Поставим breakpoint в её начало, и запустим wkhtmltopdf по-новой. Выполняя styleForElement() пошагово, доходим до вызова matchUARules(firstUARule, lastUARule); — и там внутри нечто весьма интересное:

        // First we match rules from the user agent sheet.
        RuleSet* userAgentStyleSheet = m_medium->mediaTypeMatchSpecific("print")
            ? defaultPrintStyle : defaultStyle;
    

    как легко убедиться в отладчике, m_medium.m_ptr->m_mediaType.m_impl.m_ptr->m_data содержит строку «screen», несмотря на использование --print-media-type. Инициализируется CSSStyleSelector::m_medium прямо в конструкторе:

        FrameView* view = document->view();
        if (view)
            m_medium = adoptPtr(new MediaQueryEvaluator(view->mediaType()));
        else
            m_medium = adoptPtr(new MediaQueryEvaluator("all"));
    

    Ставим breakpoint в это место, перезапускаем wkhtmltopdf, и убеждаемся, что m_mediaType инициализируется сразу в «screen». Если же зайти внутрь вызова view->mediaType(), то увидим:

    String FrameView::mediaType() const
    {
        // See if we have an override type.
        String overrideType = m_frame->loader()->client()->overrideMediaType();
        if (!overrideType.isNull())
            return overrideType;
        return m_mediaType;
    }
    

    Заходим ещё глубже:

    String FrameLoaderClientQt::overrideMediaType() const
    {
        return String();
    }
    

    Выходит, что FrameLoaderClientQt тупо не позволяет выбрать media type, отличный от m_mediaType, заданного в конструкторе FrameView в виде захардкоженной строки «screen»; а метод FrameView::setMediaType, позволяющий изменить m_mediaType, не выведен наружу в API класса QWebFrame, через который со фреймом работает клиент.
    На счастье, у FrameLoaderClientQt есть ссылка m_webFrame на объект QWebFrame, который создаётся (методом QWebPagePrivate::createMainFrame) из объекта wkhtmltopdf::MyQWebPage, определённого вне Qt. Значит, чтобы починить баг, достаточно двух изменений в коде:

    • исправить FrameLoaderClientQt::overrideMediaType, чтобы он не сразу возвращал пустую строку, а перенаправлял вызов объекту m_webFrame->page();
    • исправить wkhtmltopdf::MyQWebPage, чтобы при использовании --print-media-type он возвращал вызову overrideMediaType() строку «print».

    Два этих изменения (одно в форке Qt, второе в самом wkhtmltopdf) я предложил в виде пулл-реквестов в середине мая — но, как и можно было ожидать, никто на них внимания не обратил, как не обращал и на мой баг-репорт. Зато у меня появилась возможность собрать самому для себя исправленную wkhtmltopdf, в которой для распечатки изображений, отсутствующих в экранной версии, не требуются костыли навроде <div class="force-load" />.



    RUVDS.com
    RUVDS – хостинг VDS/VPS серверов

    Комментарии 32

      +3

      опенсурс такой опенсурс. Плюсанул оба PR. А к мейтейнерам пробовали обращаться напрямую?

        +9
        Если бы это был не опенсурс, то возможность собрать самому для себя исправленную wkhtmltopdf отсутствовала бы в принципе как класс.
          +4

          В бесплатном случае, скорее всего, — да, а при платной версии — зависит от условий.
          Притом, никто и не скрывает, что бесплатное — это на свой страх и риск.

            +3
            В бесплатном случае, скорее всего, — да, а при платной версии — зависит от условий.

            Сейчас работаем с одним итальянским оборудованием с мега-скудным API, которое очень сильно отличается в худшую сторону (нет 90% базовых возможностей) от других производителей. Узнали, сколько будет стоить подтянуть их скудность хотя бы до уровня самых простых вариантов от других производителей.


            Получили в ответ письмо, что это будет стоить нам (!) $750/час (!!) в течение месяца (!!!) только для оценки (!!?) задачи, чтобы через месяц нам назвали окончательный срок (!!!!). При том, что без этого api их оборудование в принципе неконкурентноспособно на рынке.

              +2
              без этого api их оборудование в принципе неконкурентноспособно на рынке.

              Если судь чисто по комменту — вы по каким то причинам с ними работаете. И видимо не вы одни. И похоже отток клиентов не такой, чтобы напрягаться и подтягивать. Т.е. формально они таки конкурентноспособны :)

                0

                Речь про кинооборудование. Часть кинотеатров закупила его потому что дешево, оказалось что у него нет бОльшей части API для удаленного управления (если грубо — то есть только play/stop). Чтобы убрать механика и заменить роботом этого мало.

                  +3

                  Конкурентное преимущество — цена, а не богатый API :)

                +2

                За такие деньги дешевле реверс инженера взять который вам что нужно допишет

                  0
                  У буржуев за реверсинг и «недополученную прибыль» в результате него — могут и к ответственности привлечь.
                0

                Ммм… Вы много знаете end-user коммерческого виндового софта к которому при покупке дадут исходники?

            +2

            Попробовал распечатать локальный html-файл с картинками в Chrome. Фоновые картинки не подгрузились.


            Где-то советуют использовать стили


            body {
              -webkit-print-color-adjust: exact !important;
              print-color-adjust: exact !important;
            }

            Но мне они не помогли. Задал явно формат:


            @page {
              size: A4 portrait;
            }

            и заработало. Странно.


            Могу ошибаться, но в wkhtmltopdf (как и в PhantomJS) используется древняя версия движка Webkit, еще та, что была до разделения на собственно Webkit и Blink (который сейчас в Chrome). Поэтому для печати лучше использовать Puppeteer или Playwright.

              0
              Судя по комментариям, печать в PDF через Puppeteer во много раз медленнее, чем через wkhtmltopdf.
                0

                Зато можно использовать современные технологии для подготовки материала, например React :)

                  +2
                  Это чтобы замедлить печать ещё сильнее?
                    0

                    Чтобы ускорить разработку и модификацию печатных форм

              +10
              к wkhtmltopdf прилагается собственный форк Qt 4.8 с несколькими десятками изменений, внесённых специально для wkhtmltopdf.
              Похоже разработчики Qt так же проигнорировали багрепорты от разработчиков wkhtmltopdf. И они обиделись ;)
                +5
                Заголовок спойлера

                А как они WebKit обновляют, если там свой форк Qt4 и QtWebKit (зачем, вопрос отдельный), Qt4 официально не поддерживается давно (с 2015).

                  0
                  Никак не обновляют, к сожалению.
                  +1

                  Переезжайте на puppeteer как можно скорее. Мы ловили разные баги, плоть до полного зависания определенных страниц.

                    0
                    Компиляция Linux-версий в Docker под Windows, скорее всего, невозможна в принципе

                    Да ладно?


                    Для командной строки Windows "../src\qt\configure" означает команду… с ключом /src\qt\configure — и естественно, что такая команда приводит к ошибке. Это значит, что удалённые вызовы os.path.abspath были не совсем лишними, и внутри def inside_vm(): придётся дважды обернуть использование src_dir в вызов os.path.abspath — при запуске qt/configure в самом начале сборки, и при запуске qmake в самом конце.

                    Вообще-то, достаточно заключить путь в двойные кавычки. Это, кстати, и после os.path.abspath надо обязательно делать, ведь в абсолютном пути могут быть пробелы.

                      0

                      Там неприятная история, что вроде как (но это не точно) — надо переключаться между режимом windows и linux контейнеров

                      0
                      А я столкнулся с другим багом. Если через wkhtmltopdf создавать объемный PDF-документ с колонтитулами, то при определенном объеме (несколько сот листов) wkhtmltopdf вылетает с ошибкой. В итоге мне пришлось отказаться от колонтитулов. Проблема, судя по отзывам, старая, но год назад так и не была исправлена. Как сейчас не знаю, надо будет проверить.
                        +1

                        Заставить (lib)webkit(gtk) печатать (в ps file) или экспортировать в pdf, безо всяких wkhtmltopdf, дело не абы какое сложное (сишный файлик со страницей кода)… Но зачем?
                        Когда есть куча способов прямо "иcкоропки", хоть тем же libre- или open-office:


                        soffice --convert-to --help
                        soffice --headless --norestore --writer --convert-to pdf --outdir "$target_path" *.html

                        Ну уж а модулей для всяких скриптовых чуть ли не десятки понаписаны, ну например WeasyPrint для python и тому подобные.

                          +4

                          У офисов (у всех какие пробовал, включая MS) есть проблемы с импортом html, да и у библиотек — не основная их задача спеки там всякие соблюдать. По сути нужно отдельные диалекты html/css изучать, чтобы готовить печатные документы через них из html/css, чтобы использовать имеющийся опыт команды в веб-вёрстке. Или учить кого-то работать непосредственно с pdf или офиснім форматом каким-то.

                            0

                            Ну у меня несколько "обратная" статистика… Да, офисы не всегда корректно импортируют сложные html, но зато они заточены под печать и работу с докуменами, а браузерные движки (особливо старые, например как в wkhtmltopdf) не всегда хорошо отображают то для печати (или экспорта в pdf), игнорят страничную разметку или стили, и т.д.
                            В любом случае проще поправить стиль или html, чем прыгать с бубном вокруг wkhtmltopdf, причем держа в уме что танцы вероятно повторятся с выкатом следующей версии.

                              0

                              Если не получается с браузерными движками, то проще уже генерировать непосредственно odt, docx или pdf — через их модель непосредственно. wkhtmltopdf я тоже не люблю из-за старости. сhrome/ium headless — как только начинаются маты с wkhtmltopdf, а потом работа или с офисом непосредственно, или с его файлами, без прокладки в виде html/css

                          +1

                          нам wkhtmltopdf не подошел из-за древности движка. если страничка генерится не только для печати, но и должна функционировать как интерактивный отчет, то без современного движка никак. chrome --headless --print-to-pdf, как вариант. создание pdf в 1000 страниц A4 c табличками и plotly графиками занимает ~8 минут. в нашем конкртеном случае это было приемлемо.

                            0
                            Забавно и поздравления, что удалось собрать и найти решение! Была схожая ситуация, где нужны была мультиплатформенность и еще пара особых треобваний, но без pdf. В итоге сделал поверх Qt свой message loop вне QApplication как и в wkhtmltopdf. Сам Qt на бол-ве платформ неплохо собирается, так что это реально все упростило без контейнеров, виртуальных машин и пр.
                              0
                              wkhtmltopdf имеет свойство разваливаться если на вход ему приходит HTML больше чем 50мб. Видел в проде случай когда преобразование 80мб XML в HTML выдало HTML размером в 120мб, который за 20 часов так и не смог перевариться даже скушав 24гб оперативы. В один момент просто умер.

                              Бегите с этого ужаса. Я перевёл на openhtmltopdf от danfickle (https://github.com/danfickle/openhtmltopdf), скорость увеличилась в 200 раз.
                                +1
                                Долгое время называл его wtftopdf
                                  0
                                  oldadmin
                                  Сколько времени потратили чтобы добиться результата?
                                    +1
                                    Полтора месяца от того, как начал искать баг, до того, как мейнтейнер принял мой патч.

                                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                  Самое читаемое