22
22
23
23
import sys
24
24
import os
25
- import tempfile
25
+ import subprocess
26
26
from PyQt4 .QtCore import *
27
27
from PyQt4 .QtGui import *
28
+ from PyQt4 .QtSvg import QSvgRenderer , QSvgGenerator
28
29
29
30
from qgis .core import *
30
31
31
32
from utilities import (
32
33
unittest ,
33
34
expectedFailure ,
35
+ getTempfilePath ,
36
+ getExecutablePath ,
34
37
)
35
38
36
39
from test_qgspallabeling_base import TestQgsPalLabeling , runSuite
39
42
suiteTests
40
43
)
41
44
45
+ # look for Poppler, then muPDF PDF-to-image utility
46
+ for util in ['pdftoppm' , 'mudraw' ]:
47
+ PDFUTIL = getExecutablePath (util )
48
+ if PDFUTIL :
49
+ break
50
+
51
+
52
+ def skip_if_not_pdf_util (): # skip test class decorator
53
+ if PDFUTIL :
54
+ return lambda func : func
55
+ return unittest .skip ('\n PDF-to-image utility not found on PATH\n '
56
+ 'Install Poppler or muPDF utilities\n \n ' )
57
+
58
+
59
+ # output kind enum
60
+ # noinspection PyClassHasNoInit
61
+ class OutputKind ():
62
+ Img , Svg , Pdf = range (3 )
63
+
42
64
43
65
# noinspection PyShadowingNames
44
66
class TestComposerBase (TestQgsPalLabeling ):
45
67
68
+ layer = None
69
+ """:type: QgsVectorLayer"""
70
+
46
71
@classmethod
47
72
def setUpClass (cls ):
48
- TestQgsPalLabeling .setUpClass ()
73
+ if not cls ._BaseSetup :
74
+ TestQgsPalLabeling .setUpClass ()
49
75
# the blue background (set via layer style) to match renderchecker's
50
- cls . _BkgrdLayer = TestQgsPalLabeling .loadFeatureLayer ('background' )
76
+ TestQgsPalLabeling .loadFeatureLayer ('background' , True )
51
77
cls ._CheckMismatch = 0 # mismatch expected for crosscheck
52
- cls ._CheckGroup = ''
78
+ cls ._TestImage = ''
79
+ cls ._TestKind = '' # img|svg|pdf
80
+ cls ._ImgW = 600
81
+ cls ._ImgH = 400
82
+ cls ._ImgDpi = 72
53
83
54
84
@classmethod
55
85
def tearDownClass (cls ):
56
86
"""Run after all tests"""
57
87
TestQgsPalLabeling .tearDownClass ()
88
+ cls ._MapRegistry .removeMapLayer (cls .layer .id ())
89
+ cls .layer = None
90
+
91
+ def setUp (self ):
92
+ """Run before each test."""
93
+ TestQgsPalLabeling .setDefaultEngineSettings ()
94
+ self .lyr = self .defaultLayerSettings ()
95
+ self ._TestImage = ''
58
96
59
- def get_composer_image (self , width = 600 , height = 400 , dpi = 72 ):
97
+ # noinspection PyUnusedLocal
98
+ def checkTest (self , ** kwargs ):
99
+ self .lyr .writeToLayer (self .layer )
100
+ res_m , self ._TestImage = self .get_composer_output (self ._TestKind )
101
+ self .saveContolImage (self ._TestImage )
102
+ self .assertTrue (res_m , 'Failed to retrieve/save output from composer' )
103
+ self .assertTrue (* self .renderCheck (mismatch = self ._CheckMismatch ,
104
+ imgpath = self ._TestImage ))
105
+
106
+ def _set_up_composition (self , width , height , dpi ):
60
107
# set up composition and add map
61
- comp = QgsComposition (self ._MapSettings )
108
+ # TODO: how to keep embedded map from being anti-aliased twice?
109
+ # self._MapSettings.setFlag(QgsMapSettings.Antialiasing, False)
110
+ self ._c = QgsComposition (self ._MapSettings )
62
111
""":type: QgsComposition"""
63
- comp .setPrintResolution (dpi )
112
+ self . _c .setPrintResolution (dpi )
64
113
# 600 x 400 px = 211.67 x 141.11 mm @ 72 dpi
65
114
# TODO: figure out why this doesn't work and needs fudging
66
115
# probably need sets of fudgyness per dpi group (72, 150, 300)?
67
116
paperw = round ((width * 25.4 / dpi ) + 0.05 , 0 )
68
117
paperh = round ((height * 25.4 / dpi ) + 0.05 , 1 )
69
- comp .setPaperSize (paperw , paperh )
70
- compmap = QgsComposerMap (
71
- comp , 0 , 0 , comp . paperWidth (), comp .paperHeight ())
118
+ self . _c .setPaperSize (paperw , paperh )
119
+ self . _cmap = QgsComposerMap (
120
+ self . _c , 0 , 0 , self . _c . paperWidth (), self . _c .paperHeight ())
72
121
""":type: QgsComposerMap"""
73
- compmap .setFrameEnabled (False )
74
- comp .addComposerMap (compmap )
75
- compmap .setNewExtent (self .aoiExtent ())
122
+ self ._cmap .setFrameEnabled (False )
123
+ self ._c .addComposerMap (self ._cmap )
124
+ self ._cmap .setNewExtent (self .aoiExtent ())
125
+ self ._c .setPlotStyle (QgsComposition .Print )
76
126
77
- comp .setPlotStyle (QgsComposition .Print )
78
- dpi = comp .printResolution ()
127
+ # noinspection PyUnusedLocal
128
+ def _get_composer_image (self , width , height , dpi ):
129
+ # dpi = self._c.printResolution() # why did I add this before?
79
130
dpmm = dpi / 25.4
80
- img_width = int (dpmm * comp .paperWidth ())
81
- img_height = int (dpmm * comp .paperHeight ())
131
+ img_width = int (dpmm * self . _c .paperWidth ())
132
+ img_height = int (dpmm * self . _c .paperHeight ())
82
133
83
134
# create output image and initialize it
84
135
image = QImage (QSize (img_width , img_height ), QImage .Format_ARGB32 )
@@ -88,77 +139,173 @@ def get_composer_image(self, width=600, height=400, dpi=72):
88
139
89
140
# render the composition
90
141
p = QPainter (image )
91
- p .setRenderHint (QPainter .Antialiasing )
92
- src = QRectF (0 , 0 , comp .paperWidth (), comp .paperHeight ())
142
+ p .setRenderHint (QPainter .HighQualityAntialiasing , False )
143
+ p .setRenderHint (QPainter .SmoothPixmapTransform , False )
144
+ p .setRenderHint (QPainter .Antialiasing , False )
145
+ p .setRenderHint (QPainter .TextAntialiasing , False )
146
+ src = QRectF (0 , 0 , self ._c .paperWidth (), self ._c .paperHeight ())
93
147
trgt = QRectF (0 , 0 , img_width , img_height )
94
- comp .render (p , trgt , src )
148
+ self . _c .render (p , trgt , src )
95
149
p .end ()
96
150
97
- tmp = tempfile .NamedTemporaryFile (suffix = ".png" , delete = False )
98
- filepath = tmp .name
99
- tmp .close ()
151
+ filepath = getTempfilePath ('png' )
152
+ res = image .save (filepath , 'png' )
153
+ if not res :
154
+ os .unlink (filepath )
155
+ filepath = ''
156
+
157
+ return res , filepath
100
158
101
- res = image .save (filepath , "png" )
159
+ def _get_composer_svg_image (self , width , height , dpi ):
160
+ # from qgscomposer.cpp, QgsComposer::on_mActionExportAsSVG_triggered,
161
+ # line 1909, near end of function
162
+ svgpath = getTempfilePath ('svg' )
163
+ temp_size = os .path .getsize (svgpath )
164
+ svg_g = QSvgGenerator ()
165
+ # noinspection PyArgumentList
166
+ svg_g .setTitle (QgsProject .instance ().title ())
167
+ svg_g .setFileName (svgpath )
168
+ # width and height in pixels
169
+ svg_w = int (self ._c .paperWidth () * self ._c .printResolution () / 25.4 )
170
+ svg_h = int (self ._c .paperHeight () * self ._c .printResolution () / 25.4 )
171
+ svg_g .setSize (QSize (svg_w , svg_h ))
172
+ svg_g .setViewBox (QRect (0 , 0 , svg_w , svg_h ))
173
+ # because the rendering is done in mm, convert the dpi
174
+ svg_g .setResolution (self ._c .printResolution ())
175
+
176
+ sp = QPainter (svg_g )
177
+ self ._c .renderPage (sp , 0 )
178
+ sp .end ()
179
+
180
+ if temp_size == os .path .getsize (svgpath ):
181
+ # something went pear-shaped
182
+ return False , ''
183
+
184
+ svgr = QSvgRenderer (svgpath )
185
+ image = QImage (width , height , QImage .Format_ARGB32 )
186
+ image .fill (QColor (152 , 219 , 249 ).rgb ())
187
+ image .setDotsPerMeterX (dpi / 25.4 * 1000 )
188
+ image .setDotsPerMeterY (dpi / 25.4 * 1000 )
189
+ p = QPainter (image )
190
+ svgr .render (p )
191
+ p .end ()
192
+
193
+ filepath = getTempfilePath ('png' )
194
+ res = image .save (filepath , 'png' )
102
195
if not res :
103
196
os .unlink (filepath )
104
197
filepath = ''
198
+ # TODO: remove .svg file as well?
105
199
106
200
return res , filepath
107
201
202
+ def _get_composer_pdf_image (self , width , height , dpi ):
203
+ pdfpath = getTempfilePath ('pdf' )
204
+ temp_size = os .path .getsize (pdfpath )
205
+ p = QPrinter ()
206
+ p .setOutputFormat (QPrinter .PdfFormat )
207
+ p .setOutputFileName (pdfpath )
208
+ p .setPaperSize (QSizeF (self ._c .paperWidth (), self ._c .paperHeight ()),
209
+ QPrinter .Millimeter )
210
+ p .setFullPage (True )
211
+ p .setColorMode (QPrinter .Color )
212
+ p .setResolution (self ._c .printResolution ())
213
+
214
+ pdf_p = QPainter (p )
215
+ page_mm = p .pageRect (QPrinter .Millimeter )
216
+ page_px = p .pageRect (QPrinter .DevicePixel )
217
+ self ._c .render (pdf_p , page_px , page_mm )
218
+ pdf_p .end ()
219
+
220
+ if temp_size == os .path .getsize (pdfpath ):
221
+ # something went pear-shaped
222
+ return False , ''
223
+
224
+ filepath = getTempfilePath ('png' )
225
+ filebase = os .path .join (os .path .dirname (filepath ),
226
+ os .path .splitext (os .path .basename (filepath ))[0 ])
227
+ # pdftoppm -singlefile -r 72 -x 0 -y 0 -W 600 -H 400 -png in.pdf pngbase
228
+ # mudraw -o out.png -r 72 -w 600 -h 400 -c rgb[a] in.pdf
229
+ if PDFUTIL == 'pdftoppm' :
230
+ call = [
231
+ 'pdftoppm' , '-singlefile' ,
232
+ '-r' , str (dpi ),
233
+ '-x' , str (0 ), '-y' , str (0 ),
234
+ '-W' , str (width ), '-H' , str (height ),
235
+ '-png' , pdfpath , filebase
236
+ ]
237
+ elif PDFUTIL == 'mudraw' :
238
+ call = [
239
+ 'mudraw'
240
+ '-o' , filepath ,
241
+ '-c' , 'rgba' ,
242
+ '-r' , str (dpi ),
243
+ '-w' , str (width ), '-h' , str (height ),
244
+ pdfpath
245
+ ]
246
+ else :
247
+ return False , ''
108
248
109
- class TestComposerPoint ( TestComposerBase , TestPointBase ):
249
+ res = subprocess . check_call ( call )
110
250
111
- _TestImage = ''
251
+ if not res :
252
+ os .unlink (filepath )
253
+ filepath = ''
254
+
255
+ return res , filepath
256
+
257
+ def get_composer_output (self , kind ):
258
+ width , height , dpi = self ._ImgW , self ._ImgH , self ._ImgDpi
259
+ self ._set_up_composition (width , height , dpi )
260
+ if kind == OutputKind .Svg :
261
+ return self ._get_composer_svg_image (width , height , dpi )
262
+ elif kind == OutputKind .Pdf :
263
+ return self ._get_composer_pdf_image (width , height , dpi )
264
+ else : # 'img'
265
+ return self ._get_composer_image (width , height , dpi )
266
+
267
+
268
+ class TestComposerPointBase (TestComposerBase , TestPointBase ):
112
269
113
270
@classmethod
114
271
def setUpClass (cls ):
115
272
TestComposerBase .setUpClass ()
116
273
cls .layer = TestQgsPalLabeling .loadFeatureLayer ('point' )
117
274
118
- def setUp (self ):
119
- """Run before each test."""
120
- self .configTest ('pal_composer' , 'sp' )
121
- TestQgsPalLabeling .setDefaultEngineSettings ()
122
- self .lyr = self .defaultSettings ()
123
- self ._TestImage = ''
124
-
125
- def tearDown (self ):
126
- """Run after each test."""
127
- pass
128
275
129
- def checkTest (self , ** kwargs ):
130
- self .lyr .writeToLayer (self .layer )
131
- res_m , self ._TestImage = self .get_composer_image ()
132
- self .saveContolImage (self ._TestImage )
133
- self .assertTrue (res_m , 'Failed to retrieve/save image from test server' )
134
- # gp = kwargs['grpprefix'] if 'grpprefix' in kwargs else ''
135
- self .assertTrue (* self .renderCheck (mismatch = self ._CheckMismatch ,
136
- imgpath = self ._TestImage ,
137
- grpprefix = self ._CheckGroup ))
276
+ class TestComposerImagePoint (TestComposerPointBase ):
138
277
278
+ def setUp (self ):
279
+ """Run before each test."""
280
+ super (TestComposerImagePoint , self ).setUp ()
281
+ self ._TestKind = OutputKind .Img
282
+ self .configTest ('pal_composer' , 'sp_img' )
283
+ # TODO: due to double antialiasing?
284
+ self ._CheckMismatch = 2700 # comment to PAL_REPORT difference
139
285
140
- class TestComposerVsCanvasPoint (TestComposerPoint ):
141
286
142
- @classmethod
143
- def setUpClass (cls ):
144
- TestComposerPoint .setUpClass ()
145
- cls ._CheckGroup = 'pal_canvas'
146
- # rounding errors in composer; antialiasing?
147
- cls ._CheckMismatch = 2700
148
- # cls._CheckMismatch = 0 # uncomment to PAL_REPORT actual difference
287
+ class TestComposerImageVsCanvasPoint (TestComposerPointBase ):
149
288
150
289
def setUp (self ):
151
- super (TestComposerVsCanvasPoint , self ).setUp ()
290
+ """Run before each test."""
291
+ super (TestComposerImageVsCanvasPoint , self ).setUp ()
292
+ self ._TestKind = OutputKind .Img
293
+ self .configTest ('pal_canvas' , 'sp' )
294
+ # TODO: due to double antialiasing?
152
295
if 'test_background_svg' in self .id ():
153
296
self ._CheckMismatch = 3600
297
+ # self._CheckMismatch = 0 # uncomment to PAL_REPORT difference
154
298
155
299
156
300
if __name__ == '__main__' :
157
301
# NOTE: unless PAL_SUITE env var is set all test class methods will be run
158
302
# SEE: test_qgspallabeling_tests.suiteTests() to define suite
159
- suite = (
160
- ['TestComposerPoint.' + t for t in suiteTests ()['sp_suite' ]] +
161
- ['TestComposerVsCanvasPoint.' + t for t in suiteTests ()['sp_vs_suite' ]]
162
- )
303
+ st = suiteTests ()
304
+ sp_i = ['TestComposerImagePoint.' + t for t in st ['sp_suite' ]]
305
+ sp_ivs = ['TestComposerImageVsCanvasPoint.' + t for t in st ['sp_vs_suite' ]]
306
+ suite = []
307
+ # extended separately for finer control of PAL_SUITE (comment-out undesired)
308
+ suite .extend (sp_i )
309
+ suite .extend (sp_ivs )
163
310
res = runSuite (sys .modules [__name__ ], suite )
164
311
sys .exit (not res .wasSuccessful ())
0 commit comments