21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148 | class GetLegendRunner(MapRunner):
job_info_class = QslJobInfoLegend
def __init__(
self,
qgis: QgsApplication,
context: JobContext,
job_info: QslJobInfoLegend,
layer_cache: Optional[Dict] = None,
) -> None:
super().__init__(qgis, context, job_info, layer_cache)
@classmethod
def image_formats(cls):
return {"image/png": cls._encode_png, "image/jpeg": cls._encode_jpg}
def run(self):
logging.info(f"Executing job: {self.job_info}")
for job_layer_definition in self.job_info.job.layers:
self._provide_layer(job_layer_definition)
if not self.map_layers:
raise RuntimeError("No legend entries available for requested layers")
root = QgsLayerTree()
for layer in self.map_layers:
root.addLayer(layer)
dpi = self.job_info.job.dpi
px_per_mm = dpi / 25.4
model = QgsLayerTreeModel(root)
settings = QgsLegendSettings()
if (scale := self.job_info.job.scale) is not None:
settings.setMapScale(scale)
if not self.job_info.job.layer_title:
style = QgsLegendStyle()
style.setMargin(QgsLegendStyle.Bottom, 0)
settings.setStyle(QgsLegendStyle.Title, style)
for layer_node in root.children():
QgsLegendRenderer.setNodeLegendStyle(layer_node, QgsLegendStyle.Hidden)
renderer = QgsLegendRenderer(model, settings)
width = self.job_info.job.width
height = self.job_info.job.height
legend_size_mm = renderer.minimumSize()
default_legend_width_px = int(legend_size_mm.width() * px_per_mm)
default_legend_height_px = int(legend_size_mm.height() * px_per_mm)
if width is None and height is None:
# Default legend looks a bit too big, we scale it down by a bit
width = int(0.75 * default_legend_width_px)
height = int(0.75 * default_legend_height_px)
painter_scale = 0.75
elif width is None:
painter_scale = height / default_legend_height_px
width = int(default_legend_width_px * painter_scale)
elif height is None:
painter_scale = width / default_legend_width_px
height = int(default_legend_height_px * painter_scale)
else:
x_scale = width / (legend_size_mm.width() * px_per_mm)
y_scale = height / (legend_size_mm.height() * px_per_mm)
painter_scale = min(x_scale, y_scale)
image = QImage(width, height, QImage.Format_ARGB32)
image.setDotsPerMeterX(int(dpi * 39.37))
image.setDotsPerMeterY(int(dpi * 39.37))
image.fill(Qt.white)
painter = QPainter(image)
painter.setRenderHint(QPainter.Antialiasing, True)
painter.setRenderHint(QPainter.TextAntialiasing, True)
painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
painter.scale(painter_scale, painter_scale)
renderer.drawLegend(painter)
painter.end()
content_type, image_data = self._encode_image(
image, self.job_info.job.format.lower()
)
return JobResult(
id=self.job_info.id,
data=image_data,
content_type=content_type,
)
def _encode_image(self, image: QImage, fmt: str) -> Tuple[str, bytearray]:
"""Encodes an image in a specific mime type
Args:
image (QImage): The image to encode
fmt (str): The mime type of the format
Returns:
A tuple with mime type and bytes-like object of an encoded image in the desired format
"""
try:
fmt = fmt.lower()
encoding_method = self.image_formats()[fmt]
return fmt, encoding_method(image)
except KeyError:
raise RuntimeError(
f"Requested mimtype '{fmt}' was found in {list(self.image_formats())}."
)
@staticmethod
def _encode_png(image: QImage):
image = image.convertToFormat(QImage.Format_RGBA8888)
image_data = fpng_encode_image_to_memory(
image.constBits().asstring(image.sizeInBytes()),
image.width(),
image.height(),
0,
CompressionFlags.NONE,
)
return image_data
@staticmethod
def _encode_jpg(image: QImage):
image_data = QByteArray()
buf = QBuffer(image_data)
buf.open(QIODevice.WriteOnly)
image.save(buf, "JPG")
return image_data
|