Coverage for gws-app/gws/base/printer/worker.py: 19%

147 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 23:09 +0200

1from typing import Optional, cast 

2 

3import gws 

4import gws.base.model 

5import gws.base.job 

6import gws.lib.crs 

7import gws.gis.render 

8import gws.lib.image 

9import gws.lib.intl 

10import gws.lib.mime 

11import gws.lib.osx 

12import gws.lib.style 

13import gws.lib.uom 

14 

15_PAPER_COLOR = 'white' 

16 

17 

18class Object(gws.base.job.worker.Object): 

19 project: gws.Project 

20 tri: gws.TemplateRenderInput 

21 printer: gws.Printer 

22 template: gws.Template 

23 tmpDir: str 

24 contentPath: str 

25 

26 @classmethod 

27 def run(cls, root: gws.Root, job: gws.Job): 

28 request = gws.u.unserialize_from_path(job.payload.get('requestPath')) 

29 w = cls(root, job.user, job, request) 

30 w.work() 

31 

32 def __init__(self, root: gws.Root, user: gws.User, job: Optional[gws.Job], request: gws.PrintRequest): 

33 super().__init__(root, user, job) 

34 self.request = request 

35 self.page_number = 0 

36 self.contentPath = '' 

37 

38 def work(self): 

39 self.prepare() 

40 res = self.template.render(self.tri) 

41 self.contentPath = res.contentPath 

42 self.update_job(state=gws.JobState.complete, payload=dict(outputPath=self.contentPath)) 

43 

44 def prepare(self): 

45 self.project = cast(gws.Project, self.user.require(self.request.projectUid, gws.ext.object.project)) 

46 

47 self.tri = gws.TemplateRenderInput( 

48 project=self.project, 

49 user=self.user, 

50 notify=self.notify, 

51 ) 

52 

53 fmt = self.request.outputFormat or 'pdf' 

54 if fmt.lower() == 'pdf' or fmt == gws.lib.mime.PDF: 

55 self.tri.mimeOut = gws.lib.mime.PDF 

56 elif fmt.lower() == 'png' or fmt == gws.lib.mime.PNG: 

57 self.tri.mimeOut = gws.lib.mime.PNG 

58 else: 

59 raise gws.Error(f'invalid outputFormat {fmt!r}') 

60 

61 self.tri.locale = gws.lib.intl.locale(self.request.localeUid, self.tri.project.localeUids) 

62 self.tri.crs = gws.lib.crs.get(self.request.crs) or self.project.map.bounds.crs 

63 self.tri.maps = [self.prepare_map(self.tri, m) for m in (self.request.maps or [])] 

64 self.tri.dpi = int(min(gws.gis.render.MAX_DPI, max(self.request.dpi, gws.lib.uom.OGC_SCREEN_PPI))) 

65 

66 if self.request.type == 'template': 

67 # @TODO check dpi against configured qualityLevels 

68 self.printer = cast(gws.Printer, self.user.require(self.request.printerUid, gws.ext.object.printer)) 

69 self.template = self.printer.template 

70 else: 

71 mm = gws.lib.uom.size_px_to_mm(self.request.outputSize, gws.lib.uom.OGC_SCREEN_PPI) 

72 px = gws.lib.uom.size_mm_to_px(mm, self.tri.dpi) 

73 self.template = self.root.create_temporary( 

74 gws.ext.object.template, 

75 type='map', 

76 pageSize=(px[0], px[1], gws.Uom.px)) 

77 

78 extra = dict( 

79 project=self.project, 

80 user=self.user, 

81 scale=self.tri.maps[0].scale if self.tri.maps else 0, 

82 rotation=self.tri.maps[0].rotation if self.tri.maps else 0, 

83 dpi=self.tri.dpi, 

84 crs=self.tri.crs.srid, 

85 ) 

86 

87 # @TODO read the args feature from the request 

88 self.tri.args = gws.u.merge(self.request.args, extra) 

89 

90 n = sum(len(mp.planes) for mp in self.tri.maps) + 1 

91 self.update_job(numSteps=n) 

92 

93 def notify(self, event, details=None): 

94 job = self.get_job() 

95 if not job: 

96 return 

97 

98 if event == 'begin_plane': 

99 name = gws.u.get(details, 'layer.title') 

100 self.update_job(step=job.step + 1, stepName=name or '') 

101 return 

102 

103 if event == 'finalize_print': 

104 self.update_job(step=job.step + 1) 

105 return 

106 

107 if event == 'page_break': 

108 self.page_number += 1 

109 return 

110 

111 def prepare_map(self, tri: gws.TemplateRenderInput, mp: gws.PrintMap) -> gws.MapRenderInput: 

112 planes = [] 

113 

114 style_opts = gws.lib.style.parser.Options( 

115 trusted=False, 

116 strict=False, 

117 imageDirs=[self.root.app.webMgr.sites[0].staticRoot.dir], 

118 ) 

119 style_dct = {} 

120 

121 for p in (mp.styles or []): 

122 style = gws.lib.style.from_props(p, style_opts) 

123 if style: 

124 style_dct[style.cssSelector] = style 

125 

126 if mp.planes: 

127 for n, p in enumerate(mp.planes): 

128 pp = self.prepare_map_plane(n, p, style_dct) 

129 if pp: 

130 planes.append(pp) 

131 

132 layers = gws.u.compact( 

133 self.user.acquire(uid, gws.ext.object.layer) 

134 for uid in (mp.visibleLayers or [])) 

135 

136 return gws.MapRenderInput( 

137 backgroundColor=_PAPER_COLOR, 

138 bbox=mp.bbox, 

139 center=mp.center, 

140 crs=tri.crs, 

141 dpi=tri.dpi, 

142 notify=tri.notify, 

143 planes=planes, 

144 project=tri.project, 

145 rotation=mp.rotation or 0, 

146 scale=mp.scale, 

147 user=tri.user, 

148 visibleLayers=layers, 

149 ) 

150 

151 def prepare_map_plane(self, n, plane: gws.PrintPlane, style_dct) -> Optional[gws.MapRenderInputPlane]: 

152 opacity = 1 

153 s = plane.get('opacity') 

154 if s is not None: 

155 opacity = float(s) 

156 if opacity == 0: 

157 return 

158 

159 if plane.type == gws.PrintPlaneType.raster: 

160 layer = cast(gws.Layer, self.user.acquire(plane.layerUid, gws.ext.object.layer)) 

161 if not layer: 

162 gws.log.warning(f'PREPARE_FAILED: plane {n}: {plane.layerUid=} not found') 

163 return 

164 if not layer.canRenderBox: 

165 gws.log.warning(f'PREPARE_FAILED: plane {n}: {plane.layerUid=} canRenderBox false') 

166 return 

167 return gws.MapRenderInputPlane( 

168 type=gws.MapRenderInputPlaneType.imageLayer, 

169 layer=layer, 

170 opacity=opacity, 

171 compositeLayerUids=plane.get('compositeLayerUids'), 

172 ) 

173 

174 if plane.type == gws.PrintPlaneType.vector: 

175 layer = cast(gws.Layer, self.user.acquire(plane.layerUid, gws.ext.object.layer)) 

176 if not layer: 

177 gws.log.warning(f'PREPARE_FAILED: plane {n}: {plane.layerUid=} not found') 

178 return 

179 if not layer.canRenderSvg: 

180 gws.log.warning(f'PREPARE_FAILED: plane {n}: {plane.layerUid=} canRenderSvg false') 

181 return 

182 style = style_dct.get(plane.cssSelector) 

183 return gws.MapRenderInputPlane( 

184 type=gws.MapRenderInputPlaneType.svgLayer, 

185 layer=layer, 

186 opacity=opacity, 

187 styles=[style] if style else [], 

188 ) 

189 

190 if plane.type == gws.PrintPlaneType.bitmap: 

191 img = None 

192 if plane.bitmapMode in ('RGBA', 'RGB'): 

193 img = gws.lib.image.from_raw_data( 

194 plane.bitmapData, 

195 plane.bitmapMode, 

196 (plane.bitmapWidth, plane.bitmapHeight)) 

197 if not img: 

198 gws.log.warning(f'PREPARE_FAILED: plane {n}: bitmap error') 

199 return 

200 return gws.MapRenderInputPlane( 

201 type=gws.MapRenderInputPlaneType.image, 

202 image=img, 

203 opacity=opacity, 

204 ) 

205 

206 if plane.type == gws.PrintPlaneType.url: 

207 img = gws.lib.image.from_data_url(plane.url) 

208 if not img: 

209 gws.log.warning(f'PREPARE_FAILED: plane {n}: url error') 

210 return 

211 return gws.MapRenderInputPlane( 

212 type=gws.MapRenderInputPlaneType.image, 

213 image=img, 

214 opacity=opacity, 

215 ) 

216 

217 if plane.type == gws.PrintPlaneType.features: 

218 model = self.root.app.modelMgr.default_model() 

219 used_styles = {} 

220 

221 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=self.user) 

222 features = [] 

223 

224 for props in plane.features: 

225 feature = model.feature_from_props(props, mc) 

226 if not feature or not feature.shape(): 

227 continue 

228 feature.cssSelector = feature.cssSelector or plane.cssSelector 

229 if feature.cssSelector in style_dct: 

230 used_styles[feature.cssSelector] = style_dct[feature.cssSelector] 

231 features.append(feature) 

232 

233 if not features: 

234 gws.log.warning(f'PREPARE_FAILED: plane {n}: no features') 

235 return 

236 

237 return gws.MapRenderInputPlane( 

238 type=gws.MapRenderInputPlaneType.features, 

239 features=features, 

240 opacity=opacity, 

241 styles=list(used_styles.values()), 

242 ) 

243 

244 if plane.type == gws.PrintPlaneType.soup: 

245 return gws.MapRenderInputPlane( 

246 type=gws.MapRenderInputPlaneType.svgSoup, 

247 soupPoints=plane.soupPoints, 

248 soupTags=plane.soupTags, 

249 opacity=opacity, 

250 ) 

251 

252 raise gws.Error(f'invalid plane type {plane.type!r}')