Source code for gws.common.printer.job

import io
import os
import time
import base64
import pickle
from PIL import Image

import gws
import gws.common.printer.types as pt
import gws.common.style
import gws.config
import gws.gis.feature
import gws.gis.render
import gws.gis.renderview
import gws.server.spool
import gws.tools.date
import gws.tools.job
import gws.tools.os2
import gws.tools.pdf
import gws.tools.units as units

import gws.types as t


[docs]class StatusParams(t.Params): jobUid: str
[docs]class StatusResponse(t.Response): jobUid: str progress: int state: gws.tools.job.State steptype: str stepname: str url: str
[docs]class PreparedSection(t.Data): center: t.Point context: dict items: t.List[t.MapRenderInputItem]
[docs]def start(req: t.IRequest, p: pt.PrintParams) -> gws.tools.job.Job: job = _create(req, p) gws.server.spool.add(job) return job
[docs]def status(job) -> StatusResponse: return StatusResponse( jobUid=job.uid, state=job.state, progress=job.progress, steptype=job.steptype or '', stepname=job.stepname or '', url=f'{gws.SERVER_ENDPOINT}/cmd/printerHttpGetResult/jobUid/{job.uid}/projectUid/{job.project_uid}', )
## def _create(req: t.IRequest, p: pt.PrintParams) -> gws.tools.job.Job: cleanup() job_uid = gws.random_string(64) base_dir = gws.ensure_dir(gws.PRINT_DIR + '/' + job_uid) req_path = base_dir + '/request.pickle' gws.write_file_b(req_path, pickle.dumps(p)) return gws.tools.job.create( uid=job_uid, user=req.user, project_uid=p.projectUid, worker=__name__ + '._worker') def _worker(root: t.IRootObject, job: gws.tools.job.Job): job_uid = job.uid base_dir = gws.PRINT_DIR + '/' + job_uid req_path = base_dir + '/request.pickle' params = pickle.loads(gws.read_file_b(req_path)) job.update(state=gws.tools.job.State.running) w = _Worker(root, job, base_dir, params, job.user) w.run() # remove jobs older than that _MAX_LIFETIME = 3600 * 1
[docs]def cleanup(): for p in os.listdir(gws.PRINT_DIR): d = gws.PRINT_DIR + '/' + p age = int(time.time() - gws.tools.os2.file_mtime(d)) if age > _MAX_LIFETIME: gws.tools.os2.run(['rm', '-fr', d]) gws.tools.job.remove(p) gws.log.debug(f'cleaned up job {p} age={age}')
_PAPER_COLOR = 'white' class _Worker: def __init__(self, root: t.IRootObject, job: gws.tools.job.Job, base_dir, p: pt.PrintParams, user: t.IUser): self.root = root self.job = job self.base_dir = base_dir self.user = user self.format = p.format or 'pdf' self.project = t.cast(t.IProject, self.acquire('gws.common.project', job.project_uid)) self.locale = p.locale if self.locale not in self.project.locales: self.locale = self.project.locales[0] self.view_scale = p.scale or 1 self.view_rotation = p.rotation or 0 self.view_crs = p.crs if not self.view_crs and self.project: self.view_crs = self.project.map.crs if not self.view_crs: raise ValueError('no crs can be found') self.template: t.Optional[t.ITemplate] = None self.legend_layer_uids = p.legendLayers or [] if p.type == 'template': self.template = t.cast(t.ITemplate, self.acquire('gws.ext.template', p.templateUid)) if not self.template: raise ValueError(f'cannot find template uid={p.templateUid!r}') # force dpi=OGC_SCREEN_PPI for low-res printing (dpi < OGC_SCREEN_PPI) self.view_dpi = max(self.template.dpi_for_quality(p.quality or 0), units.OGC_SCREEN_PPI) self.view_size_mm = self.template.map_size elif p.type == 'map': self.view_dpi = max(gws.as_int(p.dpi), units.OGC_SCREEN_PPI) self.view_size_mm = units.point_px2mm((p.mapWidth, p.mapHeight), units.OGC_SCREEN_PPI) else: raise ValueError('invalid print params type') self.common_render_items = self.prepare_render_items(p.items) self.sections: t.List[PreparedSection] = [self.prepare_section(sec) for sec in p.sections] self.default_context = { 'project': self.project, 'user': self.user, 'scale': self.view_scale, 'rotation': self.view_rotation, 'locale': self.locale, 'lang': self.locale.split('_')[0], 'date': gws.tools.date.DateFormatter(self.locale), 'time': gws.tools.date.TimeFormatter(self.locale), } nsec = len(self.sections) steps = ( nsec * len(self.common_render_items) + sum(len(s.items) for s in self.sections) + nsec + 3) self.job_step(steps=steps) def run(self): try: self.run2() except gws.tools.job.PrematureTermination as e: gws.log.warn(f'job={self.job.uid} TERMINATED {e.args!r}') def run2(self): section_paths = [] self.job_step(steptype='begin') self.legends = self.render_legends() for n, sec in enumerate(self.sections): section_paths.append(self.run_section(sec, n)) self.job_step(steptype='page', stepname=str(n)) self.job_step(steptype='end') comb_path = gws.tools.pdf.concat(section_paths, f'{self.base_dir}/comb.pdf') if self.template: ctx = gws.merge(self.default_context, self.sections[0].context) ctx['page_count'] = gws.tools.pdf.page_count(comb_path) res_path = self.template.add_headers_and_footers( context=ctx, in_path=comb_path, out_path=f'{self.base_dir}/res.pdf', format='pdf', ) else: res_path = comb_path if self.format == 'png': if self.template: size = units.point_mm2px(self.template.page_size, units.PDF_DPI) else: size = units.point_mm2px(self.view_size_mm, self.view_dpi) res_path = gws.tools.pdf.to_image( in_path=res_path, out_path=res_path + '.png', size=size, format='png' ) self.get_job().update(state=gws.tools.job.State.complete, result={'path': res_path}) def run_section(self, sec: PreparedSection, n: int): renderer = gws.gis.render.Renderer() out_path = f'{self.base_dir}/sec-{n}.pdf' ri = t.MapRenderInput( items=sec.items + self.common_render_items, background_color=_PAPER_COLOR, view=gws.gis.renderview.from_center( crs=self.view_crs, center=sec.center, scale=self.view_scale, out_size=self.view_size_mm, out_size_unit='mm', rotation=self.view_rotation, dpi=self.view_dpi, ) ) for item in renderer.run(ri, self.base_dir): self.job_step(steptype='layer', stepname=item.layer.title if item.get('layer') else '') if self.template: tr = self.template.render( context=gws.merge({}, self.default_context, sec.context), format='pdf', mro=renderer.output, legends=self.legends, out_path=out_path + '.pdf', ) return tr.path map_html = gws.gis.render.output_html(renderer.output) gws.tools.pdf.render_html( '<meta charset="UTF-8"/>' + map_html, page_size=self.view_size_mm, margin=None, out_path=out_path ) return out_path def render_legends(self): if not self.template or not self.template.legend_mode: return ls = {} for layer_uid in self.legend_layer_uids: if self.template.legend_layer_uids and layer_uid not in self.template.legend_layer_uids: continue layer = t.cast(t.ILayer, self.acquire('gws.ext.layer', layer_uid)) if not layer or not layer.has_legend: continue s = self.render_legend(layer) if s: ls[layer.uid] = s return ls def render_legend(self, layer: t.ILayer): if self.template.legend_mode == t.TemplateLegendMode.image: return layer.render_legend() if self.template.legend_mode == t.TemplateLegendMode.html: return layer.render_html_legend() def prepare_section(self, sec: pt.PrintSection): context = sec.get('context', {}) if self.template and context: context = self.template.normalize_context(context) return PreparedSection( center=sec.center, context=context, items=self.prepare_render_items(sec.get('items') or []), ) def prepare_render_items(self, items: t.List[pt.PrintItem]): rs = [] for n, item in enumerate(items): ii = self.prepare_render_item(item) if not ii: gws.log.warn(f'render item {n} FAILED') continue rs.append(ii) return rs def prepare_render_item(self, item: pt.PrintItem): ii = t.MapRenderInputItem() s = item.get('opacity') if s is not None: ii.opacity = float(s) if ii.opacity == 0: return # NB: svgs must use the PDF document dpi, not the image dpi! s = item.get('style') if s: s = gws.common.style.from_props(s) if s: ii.style = s if item.type == 'raster': ii.layer = t.cast(t.ILayer, self.acquire('gws.ext.layer', item.layerUid)) if ii.layer and ii.layer.can_render_box: ii.type = t.MapRenderInputItemType.image_layer ii.sub_layers = item.get('subLayers') return ii return if item.type == 'vector': ii.layer = t.cast(t.ILayer, self.acquire('gws.ext.layer', item.layerUid)) if ii.layer and ii.layer.can_render_svg: ii.type = t.MapRenderInputItemType.svg_layer ii.dpi = units.PDF_DPI return ii return if item.type == 'bitmap': img = self.prepare_bitmap(item) if not img: return ii.type = t.MapRenderInputItemType.image ii.image = img return ii if item.type == 'url': img = self.prepare_bitmap_url(item.url) if not img: return ii.type = t.MapRenderInputItemType.image ii.image = img return ii if item.type == 'features': features = [gws.gis.feature.from_props(p) for p in item.features] features = [f for f in features if f and f.shape] if not features: return ii.type = t.MapRenderInputItemType.features ii.features = features ii.dpi = units.PDF_DPI return ii if item.type == 'fragment': ii.type = t.MapRenderInputItemType.fragment ii.fragment = t.SvgFragment( points=item.points or [], tags=item.tags or [], styles=[gws.common.style.from_props(s) for s in (item.styles or [])]) ii.dpi = units.PDF_DPI return ii if not ii.layer: return def prepare_bitmap(self, item: pt.PrintItemBitmap): if item.mode in ('RGBA', 'RGB'): return Image.frombytes(item.mode, (item.width, item.height), item.data) def prepare_bitmap_url(self, url): data_png = 'data:image/png;base64,' if url.startswith(data_png): s = url[len(data_png):] s = s.encode('utf8') s = base64.decodebytes(s) img = Image.open(io.BytesIO(s)) img.load() return img def acquire(self, klass, uid): obj = self.root.find(klass, uid) if obj and self.user.can_use(obj): return obj def get_job(self): job = gws.tools.job.get(self.job.uid) if not job: raise gws.tools.job.PrematureTermination('NOT_FOUND') if job.state != gws.tools.job.State.running: raise gws.tools.job.PrematureTermination(f'WRONG_STATE={job.state}') return job def job_step(self, **kwargs): job = self.get_job() job.update(state=gws.tools.job.State.running, step=job.step + 1, **kwargs)