def path(self): if not self._pang_ctx: self._pre_render() # here we create a new cairo.Context in order to hold the pathdata tempCairoContext = cairo.Context(cairo.RecordingSurface(cairo.CONTENT_ALPHA, None)) tempCairoContext = driver.ensure_pycairo_context(tempCairoContext) tempCairoContext.move_to(self.x, self.y - self.baseline) # in here we create a pangoCairoContext in order to display layout on it # supposedly showlayout should work, but it fills the path instead, # therefore we use layout_path instead to render the layout to pangoCairoContext # tempCairoContext.show_layout(self.layout) PangoCairo.layout_path(tempCairoContext, self.layout) # here we extract the path from the temporal cairo.Context we used to draw on the previous step pathdata = tempCairoContext.copy_path() # creates a BezierPath instance for storing new shoebot path p = BezierPath(self._bot) # parsing of cairo path to build a shoebot path for item in pathdata: cmd = item[0] args = item[1] if cmd == PATH_MOVE_TO: p.moveto(*args) elif cmd == PATH_LINE_TO: p.lineto(*args) elif cmd == PATH_CURVE_TO: p.curveto(*args) elif cmd == PATH_CLOSE_PATH: p.closepath() # cairo function for freeing path memory return p
def _pre_render(self): # we use a new CairoContext to pre render the text rs = cairo.RecordingSurface(cairo.CONTENT_ALPHA, None) cr = cairo.Context(rs) cr = driver.ensure_pycairo_context(cr) self._pang_ctx = pangocairo_create_context(cr) self.layout = PangoCairo.create_layout(cr) # layout line spacing # TODO: the behaviour is not the same as nodebox yet # self.layout.set_spacing(int(((self._lineheight-1)*self._fontsize)*Pango.SCALE)) #pango requires an int casting # we pass pango font description and the text to the pango layout self.layout.set_font_description(self._fontface) self.layout.set_text(self.text, -1) # check if max text width is set and pass it to pango layout # text will wrap, meanwhile it checks if and indent has to be applied # indent is subordinated to width because it makes no sense on a single-line text block if self.width: self.layout.set_width(int(self.width) * Pango.SCALE) if self._indent: self.layout.set_indent(self._indent * Pango.SCALE) # set text alignment if self._align == "right": self.layout.set_alignment(Pango.Alignment.RIGHT) elif self._align == "center": self.layout.set_alignment(Pango.Alignment.CENTER) elif self._align == "justify": self.layout.set_alignment(Pango.Alignment.LEFT) self.layout.set_justify(True) else: self.layout.set_alignment(Pango.Alignment.LEFT)
def path(self): if not self._pangocairo_ctx: self._pre_render() # Render path data to a temporary cairo Context cairo_ctx = cairo.Context( cairo.RecordingSurface(cairo.CONTENT_ALPHA, None)) cairo_ctx = driver.ensure_pycairo_context(cairo_ctx) cairo_ctx.move_to(self.x, self.y - self.baseline) # show_layout should work here, but fills the path instead, # instead, use layout_path to render the layout. PangoCairo.layout_path(cairo_ctx, self._pango_layout) # Parse cairo path into Shoebot BezierPath cairo_text_path = cairo_ctx.copy_path() p = BezierPath(self._bot) for item in cairo_text_path: cmd = item[0] args = item[1] if cmd == PATH_MOVE_TO: p.moveto(*args) elif cmd == PATH_LINE_TO: p.lineto(*args) elif cmd == PATH_CURVE_TO: p.curveto(*args) elif cmd == PATH_CLOSE_PATH: p.closepath() del cairo_ctx return p
def _pre_render(self): # we use a new CairoContext to pre render the text rs = cairo.RecordingSurface(cairo.CONTENT_ALPHA, None) cr = cairo.Context(rs) cr = driver.ensure_pycairo_context(cr) # apply font options if set if self.hintstyle or self.hintmetrics or self.subpixelorder or self.antialias: opts = cairo.FontOptions() # map values to Cairo constants if self.antialias: opts.set_antialias( getattr(cairo.Antialias, self.antialias.upper())) if self.hintstyle: opts.set_hint_style( getattr(cairo.HintStyle, self.hintstyle.upper())) if self.hintmetrics: opts.set_hint_metrics( getattr(cairo.HintMetrics, self.hintmetrics.upper())) if self.subpixelorder: opts.set_subpixel_order( getattr(cairo.SubpixelOrder, self.subpixelorder.upper())) cr.set_font_options(opts) self._pangocairo_ctx = pangocairo_create_context(cr) self._pango_layout = PangoCairo.create_layout(cr) # layout line spacing # TODO: the behaviour is not the same as nodebox yet # self.layout.set_spacing(int(((self.lineheight-1)*self._fontsize)*Pango.SCALE)) #pango requires an int casting # we pass pango font description and the text to the pango layout self._pango_layout.set_font_description(self._pango_fontface) if not self.markup_vars: self._pango_layout.set_text(self.text, -1) return # some of the specified settings require a Pango.Markup hack # see https://stackoverflow.com/questions/55533312/how-to-create-a-letter-spacing-attribute-with-pycairo # and https://developer.gnome.org/pango/1.46/pango-Markup.html # we want to output something like # <span letter_spacing="2048">Hello World</span> markup_styles = " ".join([ f'{setting}="{value}"' for setting, value in self.markup_vars.items() ]) self._pango_layout.set_markup( f"<span {markup_styles}>{self.text}</span>") # check if max text width is set and pass it to pango layout # text will wrap, meanwhile it checks if and indent has to be applied # indent is subordinated to width because it makes no sense on a single-line text block if self.width: self._pango_layout.set_width(int(self.width) * Pango.SCALE) if self.indent: self._pango_layout.set_indent(self.indent * Pango.SCALE) # set text alignment self._pango_layout.set_alignment(_alignment_name_to_pango(self.align)) if self.align == "justify": self._pango_layout.set_justify(True)
def create_rcontext(self, size, frame): ''' Creates a recording surface for the bot to draw on :param size: The width and height of bot ''' self.frame = frame width, height = size meta_surface = cairo.RecordingSurface(cairo.CONTENT_COLOR_ALPHA, (0, 0, width, height)) ctx = cairo.Context(meta_surface) return ctx
def contains(self, x, y): """ Return cached bounds of this Grob. If bounds are not cached, render to a meta surface, and keep the meta surface and bounds cached. """ if self._bounds: return self._bounds record_surface = cairo.RecordingSurface(cairo.CONTENT_COLOR_ALPHA, (-1, -1, 1, 1)) dummy_ctx = cairo.Context(record_surface) self._traverse(dummy_ctx) in_fill = dummy_ctx.in_fill(x, y) return in_fill
def _get_bounds(self): """ Return cached bounds of this Grob. If bounds are not cached, render to a meta surface, and keep the meta surface and bounds cached. """ if self._bounds: return self._bounds record_surface = cairo.RecordingSurface(cairo.CONTENT_COLOR_ALPHA, (-1, -1, 1, 1)) dummy_ctx = cairo.Context(record_surface) self._traverse(dummy_ctx) self._bounds = dummy_ctx.path_extents() return self._bounds
def __init__(self, bot, path=None, x=0, y=0, width=None, height=None, alpha=1.0, data=None, pathmode=CORNER, **kwargs): Grob.__init__(self, bot) ColorMixin.__init__(self, **kwargs) self.x = x self.y = y self.width = width self.height = height self.alpha = alpha self.path = path self.data = data self._pathmode = pathmode sh = sw = None # Surface Height and Width if isinstance(self.data, cairo.ImageSurface): sw = self.data.get_width() sh = self.data.get_height() self._surface = self.data else: # checks if image data is passed in command call, in this case it wraps # the data in a StringIO oject in order to use it as a file # the data itself must contain an entire image, not just pixel data # it can be useful for example to retrieve images from the web without # writing temp files (e.g. using nodebox's web library, see example 1 of the library) # if no data is passed the path is used to open a local file if self.data is None: surfaceref = self._surface_cache.get(path) if surfaceref: surface = surfaceref.surface if isinstance(surface, cairo.RecordingSurface): extents = surface.get_extents() # extents has x, y which we dont use right now sw = extents.width sh = extents.height else: sw = surface.get_width() sh = surface.get_height() elif os.path.splitext( path)[1].lower() == ".svg" and Rsvg is not None: handle = Rsvg.Handle() svg = handle.new_from_file(path) dimensions = svg.get_dimensions() sw = dimensions.width sh = dimensions.height surface = cairo.RecordingSurface(cairo.CONTENT_COLOR_ALPHA, (0, 0, sw, sh)) ctx = cairo.Context(surface) pycairo_ctx = driver.ensure_pycairo_context(ctx) svg.render_cairo(pycairo_ctx) elif os.path.splitext(path)[1].lower() == ".png": surface = cairo.ImageSurface.create_from_png(path) sw = surface.get_width() sh = surface.get_height() else: img = PILImage.open(path) if img.mode != "RGBA": img = img.convert("RGBA") sw, sh = img.size # Would be nice to not have to do some of these conversions :-\ bgra_data = img.tobytes("raw", "BGRA", 0, 1) bgra_array = array.array("B", bgra_data) surface = cairo.ImageSurface.create_for_data( bgra_array, cairo.FORMAT_ARGB32, sw, sh, sw * 4) self._surface_cache[path] = SurfaceRef(surface) else: img = PILImage.open(StringIO(self.data)) if img.mode != "RGBA": img = img.convert("RGBA") sw, sh = img.size # Would be nice to not have to do some of these conversions :-\ bgra_data = img.tobytes("raw", "BGRA", 0, 1) bgra_array = array.array("B", bgra_data) surface = cairo.ImageSurface.create_for_data( bgra_array, cairo.FORMAT_ARGB32, sw, sh, sw * 4) if width is not None or height is not None: if width: wscale = float(width) / sw else: wscale = 1.0 if height: hscale = float(height) / sh else: if width: hscale = wscale else: hscale = 1.0 self._transform.scale(wscale, hscale) self.width = width or sw self.height = height or sh self._surface = surface self._deferred_render()
def _get_context(self): self._ctx = self._ctx or cairo.Context(cairo.RecordingSurface(cairo.CONTENT_ALPHA, None)) return self._ctx