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
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +0200
1from typing import Optional, cast
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
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()
22_PAPER_COLOR = 'white'
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
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
38 self.project = cast(gws.Project, self.user.require(request.projectUid, gws.ext.object.project))
40 self.page_count = 0
42 self.tri = gws.TemplateRenderInput(
43 project=self.project,
44 user=self.user,
45 notify=self.notify,
46 )
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}')
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)))
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))
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 )
83 # @TODO read the args feature from the request
84 self.tri.args = gws.u.merge(request.args, extra)
86 num_steps = sum(len(mp.planes) for mp in self.tri.maps) + 1
88 self.update_job(state=gws.JobState.running, numSteps=num_steps)
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 }
96 if event == 'begin_plane':
97 name = gws.u.get(details, 'layer.title')
98 args['progress'] = True
99 args['stepName'] = name or ''
101 elif event == 'finalize_print':
102 args['progress'] = True
103 return self.update_job(inc=True)
105 elif event == 'begin_page':
106 self.page_count += 1
107 args['step'] = 0
108 args['stepName'] = str(self.page_count)
110 return self.update_job(**args)
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
117 def prepare_map(self, tri: gws.TemplateRenderInput, mp: gws.PrintMap) -> gws.MapRenderInput:
118 planes = []
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 = {}
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
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)
138 layers = gws.u.compact(
139 self.user.acquire(uid, gws.ext.object.layer)
140 for uid in (mp.visibleLayers or []))
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 )
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
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 )
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 )
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 )
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 )
223 if plane.type == gws.PrintPlaneType.features:
224 model = self.root.app.modelMgr.default_model()
225 used_styles = {}
227 mc = gws.ModelContext(op=gws.ModelOperation.read, target=gws.ModelReadTarget.map, user=self.user)
228 features = []
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)
239 if not features:
240 gws.log.warning(f'PREPARE_FAILED: plane {n}: no features')
241 return
243 return gws.MapRenderInputPlane(
244 type=gws.MapRenderInputPlaneType.features,
245 features=features,
246 opacity=opacity,
247 styles=list(used_styles.values()),
248 )
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 )
258 raise gws.Error(f'invalid plane type {plane.type!r}')
260 def update_job(self, **kwargs):
261 job = self.get_job()
262 if not job:
263 return
265 state = kwargs.pop('state', None)
267 if kwargs.pop('progress', None):
268 kwargs['step'] = job.payload.get('step', 0) + 1
270 job.update(state=state, payload=gws.u.merge(job.payload, kwargs))
272 def get_job(self) -> Optional[gws.lib.job.Object]:
273 if not self.jobUid:
274 return None
276 job = gws.lib.job.get(self.root, self.jobUid)
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}')
285 return job