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

164 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +0200

1from typing import Optional, cast 

2 

3import gws 

4import gws.base.model 

5import gws.gis.crs 

6import gws.gis.render 

7import gws.lib.image 

8import gws.lib.intl 

9import gws.lib.job 

10import gws.lib.mime 

11import gws.lib.osx 

12import gws.lib.style 

13import gws.lib.uom 

14 

15 

16def worker(root: gws.Root, job: gws.lib.job.Object): 

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

18 w = Object(root, job.uid, request, job.user) 

19 w.run() 

20 

21 

22_PAPER_COLOR = 'white' 

23 

24 

25class Object: 

26 jobUid: str 

27 user: gws.User 

28 project: gws.Project 

29 tri: gws.TemplateRenderInput 

30 printer: gws.Printer 

31 template: gws.Template 

32 

33 def __init__(self, root: gws.Root, job_uid: str, request: gws.PrintRequest, user: gws.User): 

34 self.jobUid = job_uid 

35 self.root = root 

36 self.user = user 

37 

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

39 

40 self.page_count = 0 

41 

42 self.tri = gws.TemplateRenderInput( 

43 project=self.project, 

44 user=self.user, 

45 notify=self.notify, 

46 ) 

47 

48 fmt = request.outputFormat or 'pdf' 

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

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

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

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

53 else: 

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

55 

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

57 self.tri.crs = gws.gis.crs.get(request.crs) or self.project.map.bounds.crs 

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

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

60 

61 if request.type == 'template': 

62 # @TODO check dpi against configured qualityLevels 

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

64 self.template = self.printer.template 

65 else: 

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

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

68 self.template = self.root.create_temporary( 

69 gws.ext.object.template, 

70 type='map', 

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

72 

73 extra = dict( 

74 project=self.project, 

75 user=self.user, 

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

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

78 dpi=self.tri.dpi, 

79 crs=self.tri.crs.srid, 

80 templateRenderInput=self.tri, 

81 ) 

82 

83 # @TODO read the args feature from the request 

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

85 

86 num_steps = sum(len(mp.planes) for mp in self.tri.maps) + 1 

87 

88 self.update_job(state=gws.JobState.running, numSteps=num_steps) 

89 

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

91 gws.log.debug(f'JOB {self.jobUid}: print.worker.notify {event=} {details=}') 

92 args = { 

93 'stepType': event, 

94 } 

95 

96 if event == 'begin_plane': 

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

98 args['progress'] = True 

99 args['stepName'] = name or '' 

100 

101 elif event == 'finalize_print': 

102 args['progress'] = True 

103 return self.update_job(inc=True) 

104 

105 elif event == 'begin_page': 

106 self.page_count += 1 

107 args['step'] = 0 

108 args['stepName'] = str(self.page_count) 

109 

110 return self.update_job(**args) 

111 

112 def run(self): 

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

114 self.update_job(state=gws.JobState.complete, resultPath=res.contentPath) 

115 return res.contentPath 

116 

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

118 planes = [] 

119 

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

121 trusted=False, 

122 strict=False, 

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

124 ) 

125 style_dct = {} 

126 

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

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

129 if style: 

130 style_dct[style.cssSelector] = style 

131 

132 if mp.planes: 

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

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

135 if pp: 

136 planes.append(pp) 

137 

138 layers = gws.u.compact( 

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

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

141 

142 return gws.MapRenderInput( 

143 backgroundColor=_PAPER_COLOR, 

144 bbox=mp.bbox, 

145 center=mp.center, 

146 crs=tri.crs, 

147 dpi=tri.dpi, 

148 notify=tri.notify, 

149 planes=planes, 

150 project=tri.project, 

151 rotation=mp.rotation or 0, 

152 scale=mp.scale, 

153 user=tri.user, 

154 visibleLayers=layers, 

155 ) 

156 

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

158 opacity = 1 

159 s = plane.get('opacity') 

160 if s is not None: 

161 opacity = float(s) 

162 if opacity == 0: 

163 return 

164 

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

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

167 if not layer: 

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

169 return 

170 if not layer.canRenderBox: 

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

172 return 

173 return gws.MapRenderInputPlane( 

174 type=gws.MapRenderInputPlaneType.imageLayer, 

175 layer=layer, 

176 opacity=opacity, 

177 subLayers=plane.get('subLayers'), 

178 ) 

179 

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

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

182 if not layer: 

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

184 return 

185 if not layer.canRenderSvg: 

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

187 return 

188 style = style_dct.get(plane.cssSelector) 

189 return gws.MapRenderInputPlane( 

190 type=gws.MapRenderInputPlaneType.svgLayer, 

191 layer=layer, 

192 opacity=opacity, 

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

194 ) 

195 

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

197 img = None 

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

199 img = gws.lib.image.from_raw_data( 

200 plane.bitmapData, 

201 plane.bitmapMode, 

202 (plane.bitmapWidth, plane.bitmapHeight)) 

203 if not img: 

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

205 return 

206 return gws.MapRenderInputPlane( 

207 type=gws.MapRenderInputPlaneType.image, 

208 image=img, 

209 opacity=opacity, 

210 ) 

211 

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

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

214 if not img: 

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

216 return 

217 return gws.MapRenderInputPlane( 

218 type=gws.MapRenderInputPlaneType.image, 

219 image=img, 

220 opacity=opacity, 

221 ) 

222 

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

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

225 used_styles = {} 

226 

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

228 features = [] 

229 

230 for props in plane.features: 

231 feature = model.feature_from_props(props, mc) 

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

233 continue 

234 feature.cssSelector = feature.cssSelector or plane.cssSelector 

235 if feature.cssSelector in style_dct: 

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

237 features.append(feature) 

238 

239 if not features: 

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

241 return 

242 

243 return gws.MapRenderInputPlane( 

244 type=gws.MapRenderInputPlaneType.features, 

245 features=features, 

246 opacity=opacity, 

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

248 ) 

249 

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

251 return gws.MapRenderInputPlane( 

252 type=gws.MapRenderInputPlaneType.svgSoup, 

253 soupPoints=plane.soupPoints, 

254 soupTags=plane.soupTags, 

255 opacity=opacity, 

256 ) 

257 

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

259 

260 def update_job(self, **kwargs): 

261 job = self.get_job() 

262 if not job: 

263 return 

264 

265 state = kwargs.pop('state', None) 

266 

267 if kwargs.pop('progress', None): 

268 kwargs['step'] = job.payload.get('step', 0) + 1 

269 

270 job.update(state=state, payload=gws.u.merge(job.payload, kwargs)) 

271 

272 def get_job(self) -> Optional[gws.lib.job.Object]: 

273 if not self.jobUid: 

274 return None 

275 

276 job = gws.lib.job.get(self.root, self.jobUid) 

277 

278 if not job: 

279 raise gws.lib.job.PrematureTermination('JOB_NOT_FOUND') 

280 if job.user.uid != self.user.uid: 

281 raise gws.lib.job.PrematureTermination('WRONG_USER {job.user.uid}') 

282 if job.state != gws.JobState.running: 

283 raise gws.lib.job.PrematureTermination(f'JOB_WRONG_STATE={job.state!r}') 

284 

285 return job