Skip to content

Commit bad1898

Browse files
committed
Refactor composer label test to work with svg, pdf and multi-resolution classes
- Initial (untested) svg and pdf render-to-image setup
1 parent 6a1b706 commit bad1898

File tree

10 files changed

+205
-58
lines changed

10 files changed

+205
-58
lines changed

tests/src/python/test_qgspallabeling_composer.py

+205-58
Original file line numberDiff line numberDiff line change
@@ -22,15 +22,18 @@
2222

2323
import sys
2424
import os
25-
import tempfile
25+
import subprocess
2626
from PyQt4.QtCore import *
2727
from PyQt4.QtGui import *
28+
from PyQt4.QtSvg import QSvgRenderer, QSvgGenerator
2829

2930
from qgis.core import *
3031

3132
from utilities import (
3233
unittest,
3334
expectedFailure,
35+
getTempfilePath,
36+
getExecutablePath,
3437
)
3538

3639
from test_qgspallabeling_base import TestQgsPalLabeling, runSuite
@@ -39,46 +42,94 @@
3942
suiteTests
4043
)
4144

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('\nPDF-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+
4264

4365
# noinspection PyShadowingNames
4466
class TestComposerBase(TestQgsPalLabeling):
4567

68+
layer = None
69+
""":type: QgsVectorLayer"""
70+
4671
@classmethod
4772
def setUpClass(cls):
48-
TestQgsPalLabeling.setUpClass()
73+
if not cls._BaseSetup:
74+
TestQgsPalLabeling.setUpClass()
4975
# the blue background (set via layer style) to match renderchecker's
50-
cls._BkgrdLayer = TestQgsPalLabeling.loadFeatureLayer('background')
76+
TestQgsPalLabeling.loadFeatureLayer('background', True)
5177
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
5383

5484
@classmethod
5585
def tearDownClass(cls):
5686
"""Run after all tests"""
5787
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 = ''
5896

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):
60107
# 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)
62111
""":type: QgsComposition"""
63-
comp.setPrintResolution(dpi)
112+
self._c.setPrintResolution(dpi)
64113
# 600 x 400 px = 211.67 x 141.11 mm @ 72 dpi
65114
# TODO: figure out why this doesn't work and needs fudging
66115
# probably need sets of fudgyness per dpi group (72, 150, 300)?
67116
paperw = round((width * 25.4 / dpi) + 0.05, 0)
68117
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())
72121
""":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)
76126

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?
79130
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())
82133

83134
# create output image and initialize it
84135
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):
88139

89140
# render the composition
90141
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())
93147
trgt = QRectF(0, 0, img_width, img_height)
94-
comp.render(p, trgt, src)
148+
self._c.render(p, trgt, src)
95149
p.end()
96150

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
100158

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')
102195
if not res:
103196
os.unlink(filepath)
104197
filepath = ''
198+
# TODO: remove .svg file as well?
105199

106200
return res, filepath
107201

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, ''
108248

109-
class TestComposerPoint(TestComposerBase, TestPointBase):
249+
res = subprocess.check_call(call)
110250

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):
112269

113270
@classmethod
114271
def setUpClass(cls):
115272
TestComposerBase.setUpClass()
116273
cls.layer = TestQgsPalLabeling.loadFeatureLayer('point')
117274

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
128275

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):
138277

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
139285

140-
class TestComposerVsCanvasPoint(TestComposerPoint):
141286

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):
149288

150289
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?
152295
if 'test_background_svg' in self.id():
153296
self._CheckMismatch = 3600
297+
# self._CheckMismatch = 0 # uncomment to PAL_REPORT difference
154298

155299

156300
if __name__ == '__main__':
157301
# NOTE: unless PAL_SUITE env var is set all test class methods will be run
158302
# 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)
163310
res = runSuite(sys.modules[__name__], suite)
164311
sys.exit(not res.wasSuccessful())

0 commit comments

Comments
 (0)