1# -*- coding: utf-8 -*-
2"""QGIS Unit tests for QgsServer WMS GetPrint.
3
4From build dir, run: ctest -R PyQgsServerWMSGetPrintOutputs -V
5
6
7.. note:: This program is free software; you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation; either version 2 of the License, or
10(at your option) any later version.
11
12"""
13__author__ = 'René-Luc DHONT'
14__date__ = '24/06/2020'
15__copyright__ = 'Copyright 2020, The QGIS Project'
16
17import os
18
19# Needed on Qt 5 so that the serialization of XML is consistent among all executions
20os.environ['QT_HASH_SEED'] = '1'
21
22import urllib.parse
23
24from qgis.testing import unittest
25from qgis.PyQt.QtCore import QSize, Qt
26from qgis.PyQt.QtGui import QImage, QPainter
27from qgis.PyQt.QtSvg import QSvgRenderer, QSvgGenerator
28
29import osgeo.gdal  # NOQA
30import tempfile
31import base64
32import subprocess
33
34from test_qgsserver import QgsServerTestBase
35from qgis.core import QgsMultiRenderChecker
36from utilities import getExecutablePath, unitTestDataPath
37
38
39class PyQgsServerWMSGetPrintOutputs(QgsServerTestBase):
40
41    def _pdf_to_png(self, pdf_file_path, rendered_file_path, page, dpi=96):
42
43        # PDF-to-image utility
44        # look for Poppler w/ Cairo, then muPDF
45        # * Poppler w/ Cairo renders correctly
46        # * Poppler w/o Cairo does not always correctly render vectors in PDF to image
47        # * muPDF renders correctly, but slightly shifts colors
48        for util in [
49            'pdftocairo',
50            # 'mudraw',
51        ]:
52            PDFUTIL = getExecutablePath(util)
53            if PDFUTIL:
54                break
55
56        # noinspection PyUnboundLocalVariable
57        if not PDFUTIL:
58            assert False, ('PDF-to-image utility not found on PATH: '
59                           'install Poppler (with Cairo)')
60
61        if PDFUTIL.strip().endswith('pdftocairo'):
62            filebase = os.path.join(
63                os.path.dirname(rendered_file_path),
64                os.path.splitext(os.path.basename(rendered_file_path))[0]
65            )
66            call = [
67                PDFUTIL, '-png', '-singlefile', '-r', str(dpi),
68                '-x', '0', '-y', '0', '-f', str(page), '-l', str(page),
69                pdf_file_path, filebase
70            ]
71        elif PDFUTIL.strip().endswith('mudraw'):
72            call = [
73                PDFUTIL, '-c', 'rgba',
74                '-r', str(dpi), '-f', str(page), '-l', str(page),
75                # '-b', '8',
76                '-o', rendered_file_path, pdf_file_path
77            ]
78        else:
79            return False, ''
80
81        print("exportToPdf call: {0}".format(' '.join(call)))
82        try:
83            subprocess.check_call(call)
84        except subprocess.CalledProcessError as e:
85            assert False, ("exportToPdf failed!\n"
86                           "cmd: {0}\n"
87                           "returncode: {1}\n"
88                           "message: {2}".format(e.cmd, e.returncode, e.message))
89
90    def _pdf_diff(self, pdf, control_image, max_diff, max_size_diff=QSize(), dpi=96):
91
92        temp_pdf = os.path.join(tempfile.gettempdir(), "%s_result.pdf" % control_image)
93
94        with open(temp_pdf, "wb") as f:
95            f.write(pdf)
96
97        temp_image = os.path.join(tempfile.gettempdir(), "%s_result.png" % control_image)
98        self._pdf_to_png(temp_pdf, temp_image, dpi=dpi, page=1)
99
100        control = QgsMultiRenderChecker()
101        control.setControlPathPrefix("qgis_server")
102        control.setControlName(control_image)
103        control.setRenderedImage(temp_image)
104        if max_size_diff.isValid():
105            control.setSizeTolerance(max_size_diff.width(), max_size_diff.height())
106        return control.runTest(control_image, max_diff), control.report()
107
108    def _pdf_diff_error(self, response, headers, image, max_diff=100, max_size_diff=QSize(),
109                        unittest_data_path='control_images', dpi=96):
110
111        reference_path = unitTestDataPath(unittest_data_path) + '/qgis_server/' + image + '/' + image + '.pdf'
112        self.store_reference(reference_path, response)
113
114        self.assertEqual(
115            headers.get("Content-Type"), 'application/pdf',
116            "Content type is wrong: %s instead of %s\n%s" % (headers.get("Content-Type"), 'application/pdf', response))
117
118        test, report = self._pdf_diff(response, image, max_diff, max_size_diff, dpi)
119
120        with open(os.path.join(tempfile.gettempdir(), image + "_result.pdf"), "rb") as rendered_file:
121            if not os.environ.get('ENCODED_OUTPUT'):
122                message = "PDF is wrong: rendered file %s/%s_result.%s" % (tempfile.gettempdir(), image, 'pdf')
123            else:
124                encoded_rendered_file = base64.b64encode(rendered_file.read())
125                message = "PDF is wrong\n%sFile:\necho '%s' | base64 -d >%s/%s_result.%s" % (
126                    report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image, 'pdf'
127                )
128
129        with open(os.path.join(tempfile.gettempdir(), image + "_result.png"), "rb") as rendered_file:
130            if not os.environ.get('ENCODED_OUTPUT'):
131                message = "Image is wrong: rendered file %s/%s_result.%s" % (tempfile.gettempdir(), image, 'png')
132            else:
133                encoded_rendered_file = base64.b64encode(rendered_file.read())
134                message = "Image is wrong\n%s\nImage:\necho '%s' | base64 -d >%s/%s_result.%s" % (
135                    report, encoded_rendered_file.strip().decode('utf8'), tempfile.gettempdir(), image, 'png'
136                )
137
138        # If the failure is in image sizes the diff file will not exists.
139        if os.path.exists(os.path.join(tempfile.gettempdir(), image + "_result_diff.png")):
140            with open(os.path.join(tempfile.gettempdir(), image + "_result_diff.png"), "rb") as diff_file:
141                if not os.environ.get('ENCODED_OUTPUT'):
142                    message = "Image is wrong: diff file %s/%s_result_diff.%s" % (tempfile.gettempdir(), image, 'png')
143                else:
144                    encoded_diff_file = base64.b64encode(diff_file.read())
145                    message += "\nDiff:\necho '%s' | base64 -d > %s/%s_result_diff.%s" % (
146                        encoded_diff_file.strip().decode('utf8'), tempfile.gettempdir(), image, 'png'
147                    )
148
149        self.assertTrue(test, message)
150
151    def _svg_to_png(svg_file_path, rendered_file_path, width):
152        svgr = QSvgRenderer(svg_file_path)
153
154        height = width / svgr.viewBoxF().width() * svgr.viewBoxF().height()
155
156        image = QImage(width, height, QImage.Format_ARGB32)
157        image.fill(Qt.transparent)
158
159        p = QPainter(image)
160        p.setRenderHint(QPainter.Antialiasing, False)
161        svgr.render(p)
162        p.end()
163
164        res = image.save(rendered_file_path, 'png')
165        if not res:
166            os.unlink(rendered_file_path)
167
168    """QGIS Server WMS Tests for GetPrint request with non default outputs"""
169
170    def test_wms_getprint_basic(self):
171
172        # Output JPEG
173        qs = "?" + "&".join(["%s=%s" % i for i in list({
174            "MAP": urllib.parse.quote(self.projectPath),
175            "SERVICE": "WMS",
176            "VERSION": "1.1.1",
177            "REQUEST": "GetPrint",
178            "TEMPLATE": "layoutA4",
179            "FORMAT": "jpeg",
180            "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031",
181            "LAYERS": "Country,Hello",
182            "CRS": "EPSG:3857"
183        }.items())])
184
185        r, h = self._result(self._execute_request(qs))
186        self._img_diff_error(r, h, "WMS_GetPrint_Basic", outputFormat='JPG')
187
188        # Output PDF
189        qs = "?" + "&".join(["%s=%s" % i for i in list({
190            "MAP": urllib.parse.quote(self.projectPath),
191            "SERVICE": "WMS",
192            "VERSION": "1.1.1",
193            "REQUEST": "GetPrint",
194            "TEMPLATE": "layoutA4",
195            "FORMAT": "pdf",
196            "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031",
197            "LAYERS": "Country,Hello",
198            "CRS": "EPSG:3857"
199        }.items())])
200
201        r, h = self._result(self._execute_request(qs))
202        self._pdf_diff_error(r, h, "WMS_GetPrint_Basic_Pdf", dpi=300)
203
204    def test_wms_getprint_selection(self):
205
206        # Output PDF
207        qs = "?" + "&".join(["%s=%s" % i for i in list({
208            "MAP": urllib.parse.quote(self.projectPath),
209            "SERVICE": "WMS",
210            "VERSION": "1.1.1",
211            "REQUEST": "GetPrint",
212            "TEMPLATE": "layoutA4",
213            "FORMAT": "pdf",
214            "LAYERS": "Country,Hello",
215            "map0:EXTENT": "-33626185.498,-13032965.185,33978427.737,16020257.031",
216            "map0:LAYERS": "Country,Hello",
217            "CRS": "EPSG:3857",
218            "SELECTION": "Country: 4"
219        }.items())])
220
221        r, h = self._result(self._execute_request(qs))
222        self._pdf_diff_error(r, h, "WMS_GetPrint_Selection_Pdf", dpi=300)
223
224
225if __name__ == '__main__':
226    unittest.main()
227