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:Usersvagrant>choco uninstall -y rsync [skipped] windows: vagrant@VAGRANT-IL06I9S C:Usersvagrant>cd "C:/Program Files/Git" windows: vagrant@VAGRANT-IL06I9S C:Program FilesGit>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.
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:Usersvagrant>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 FilesGit>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:UserstyomitchDocumentswkhtmltopdf/ windows:/c/Users/vagrant/msvc2015-win64/src command failed: exit code 1
Инициализация виртуальной машины прошла успешно, но попытка скопировать на неё репозиторий при помощи rsync столкнулась с проблемой: rsync считает двоеточие разделителем имени хоста и пути, так что путь
C:UserstyomitchDocumentswkhtmltopdf/
для rsync означает "UserstyomitchDocumentswkhtmltopdf/
на хосте 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:Usersvagrantmsvc2015-win64pkg.conanprofilesdefault) 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:Usersvagrantmsvc2015-win64pkg.conanremotes.json '..' is not recognized as an internal or external command, operable program or batch file. ../srcqtconfigure -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 "../srcqtconfigure
" означает команду ..
с ключом /srcqtconfigure
— и естественно, что такая команда приводит к ошибке. Это значит, что удалённые вызовы 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:Usersvagrantmsvc2015-win64pkg.conanprofilesdefault) [skipped] conanfile.txt: Generated graphinfo WARN: Remotes registry file missing, creating default one in C:Usersvagrantmsvc2015-win64pkg.conanremotes.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 -Igeneratorsunix -Igeneratorswin32 -Igeneratorsmac -Igeneratorssymbian -Igeneratorsintegrity [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:usersvagrantmsvc2015-win64srcqtsrc3rdpartywebkitsourcejavascriptcoreruntimePropertyMapHashTable.h(424): warning C4267: 'return': conversion from 'size_t' to 'unsigned int', possible loss of data (compiling source file c:Usersvagrantmsvc2015-win64srcqtsrc3rdpartywebkitSourceJavaScriptCoreAPIJSValueRef.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:Usersvagrantmsvc2015-win64pkg.conanremotes.json Microsoft (R) Program Maintenance Utility Version 14.00.24210.0 Copyright (C) Microsoft Corporation. All rights reserved. cd srctoolsbootstrap && "C:Program Files (x86)Microsoft Visual Studio 14.0VCBINamd64nmake.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.0VCBINamd64nmake.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:Usersvagrantmsvc2015-win64pkglibszlib1.2.11conanstablepackage63da998e3642b50bee33f4449826b2d623661505includezlib.h' Stop. NMAKE : fatal error U1077: '"C:Program Files (x86)Microsoft Visual Studio 14.0VCBINamd64nmake.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 FilesGit>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:Usersvagrantmsvc2015-win64pkg.conanprofilesdefault) 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:Usersvagrantmsvc2015-win64pkg.conanprofilesdefault *** *** 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.0VCBINamd64nmake.exe" -f Makefile.Debug Microsoft (R) Program Maintenance Utility Version 14.00.24210.0 Copyright (C) Microsoft Corporation. All rights reserved. C:Usersvagrantmsvc2015-win64buildqtbinmoc.exe -DQT_THREAD_SUPPORT -DUNICODE -DWIN32 -DQT_BUILD_CORE_LIB -DQT_NO_USING_NAMESPACE -DQT_ASCII_CAST_WARNINGS [skipped] -I"." -I"....mkspecswin32-msvc2015" -D_MSC_VER=1900 -DWIN32 c:Usersvagrantmsvc2015-win64srcqtsrccorelibanimationqabstractanimation.h -o tmpmocdebug_staticmoc_qabstractanimation.cpp NMAKE : fatal error U1077: 'C:Usersvagrantmsvc2015-win64buildqtbinmoc.exe' : return code '0xc0000135' Stop. NMAKE : fatal error U1077: '"C:Program Files (x86)Microsoft Visual Studio 14.0VCBINamd64nmake.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
.
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:Usersvagrant>"C:Program Files (x86)Microsoft Visual Studio 14.0VCBINamd64dumpbin.exe" /DEPENDENTS C:Usersvagrantmsvc2015-win64buildqtbinmoc.exe Microsoft (R) COFF/PE Dumper Version 14.00.24210.0 Copyright (C) Microsoft Corporation. All rights reserved. Dump of file C:Usersvagrantmsvc2015-win64buildqtbinmoc.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:Usersvagrant>dir WindowsSystem32vcruntime140* Volume in drive C is Windows 10 Volume Serial Number is A0AB-6559 Directory of C:WindowsSystem32 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:Usersvagrant>dir WindowsSystem32ucrtbase* Volume in drive C is Windows 10 Volume Serial Number is A0AB-6559 Directory of C:WindowsSystem32 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:Usersvagrant>dir /s ucrtbased.dll Volume in drive C is Windows 10 Volume Serial Number is A0AB-6559 Directory of C:Program Files (x86)Microsoft SDKsWindows Kits10ExtensionSDKsMicrosoft.UniversalCRT.Debug10.0.10240.0RedistDebugarm 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 SDKsWindows Kits10ExtensionSDKsMicrosoft.UniversalCRT.Debug10.0.10240.0RedistDebugarm64 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 SDKsWindows Kits10ExtensionSDKsMicrosoft.UniversalCRT.Debug10.0.10240.0RedistDebugx64 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 SDKsWindows Kits10ExtensionSDKsMicrosoft.UniversalCRT.Debug10.0.10240.0RedistDebugx86 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 Kits10binarmucrt 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 Kits10binarm64ucrt 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 Kits10binx64ucrt 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 Kits10binx86ucrt 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 debugmoc_multipageloader_p.cpp debugmoc_converter_p.cpp debugmoc_pdfconverter_p.cpp debugmoc_imageconverter_p.cpp debugmoc_pdf_c_bindings_p.cpp debugmoc_image_c_bindings_p.cpp debugmoc_converter.cpp debugmoc_multipageloader.cpp debugmoc_utilities.cpp debugmoc_pdfconverter.cpp debugmoc_imageconverter.cpp debugqrc_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 ....binwkhtmltox.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.0VCBINamd64nmake.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:Usersvagrant>dir /s libpng.lib Volume in drive C is Windows 10 Volume Serial Number is A0AB-6559 File Not Found vagrant@VAGRANT-IL06I9S C:Usersvagrant>dir /s libpng*.lib Volume in drive C is Windows 10 Volume Serial Number is A0AB-6559 Directory of C:Usersvagrantmsvc2015-win64pkglibslibpng1.6.37__packageb17b520b4b55729a7391c6b2d20631fec4cf1564lib 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 ....binwkhtmltoimage.exe Creating library ....binwkhtmltoimage.lib and object ....binwkhtmltoimage.exp mt.exe -nologo -manifest "debugwkhtmltoimage.intermediate.manifest" -outputresource:....binwkhtmltoimage.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++-проекта.
Самое интересное, конечно, в 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 ....binwkhtmltoimage.lib and object ....binwkhtmltoimage.exp mt.exe -nologo -manifest "debugwkhtmltoimage.intermediate.manifest" -outputresource:....binwkhtmltoimage.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
и смотрим, откуда она вызывается.
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" />
.
Автор: oldadmin