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" />.