Как починить все самому, если баг-репорты игнорируются: отлаживаю 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" />.

ссылка на оригинал статьи https://habr.com/ru/company/ruvds/blog/505864/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *