def _get_gradient_transform(grad_el, shape_bbox, view_box, upem) -> Affine2D: transform = map_viewbox_to_font_emsquare(view_box, upem) gradient_units = grad_el.attrib.get("gradientUnits", "objectBoundingBox") if gradient_units == "objectBoundingBox": bbox_space = Rect(0, 0, 1, 1) bbox_transform = Affine2D.rect_to_rect(bbox_space, shape_bbox) transform = Affine2D.product(bbox_transform, transform) if "gradientTransform" in grad_el.attrib: gradient_transform = Affine2D.fromstring( grad_el.attrib["gradientTransform"]) transform = Affine2D.product(gradient_transform, transform) return transform
def apply_transforms(self, inplace=False): """Naively transforms to shapes and removes the transform attribute. Naive: just applies any transform on a parent element. """ if not inplace: svg = SVG(copy.deepcopy(self.svg_root)) svg.apply_transforms(inplace=True) return svg self._update_etree() # figure out the sequence of transforms, if any, for each shape new_shapes = [] for idx, (el, (shape, )) in enumerate(self._elements()): transform = Affine2D.identity() while el is not None: if "transform" in el.attrib: transform = Affine2D.product( transform, Affine2D.fromstring(el.attrib["transform"])) el = el.getparent() if transform == Affine2D.identity(): continue new_shapes.append((idx, shape.apply_transform(transform))) for el_idx, new_shape in new_shapes: el, _ = self.elements[el_idx] self._set_element(el_idx, el, (new_shape, )) # destroy all transform attributes self.remove_attributes(["transform"], xpath="//svg:*[@transform]", inplace=True) return self
def _inherit_matrix_multiply(attrib, child, attr_name): group_transform = Affine2D.fromstring(attrib[attr_name]) if attr_name in child.attrib: transform = Affine2D.fromstring(child.attrib[attr_name]) transform = Affine2D.product(transform, group_transform) else: transform = group_transform if transform != Affine2D.identity(): child.attrib[attr_name] = transform.tostring() else: del child.attrib[attr_name]
def _parse_radial_gradient(grad_el, shape_bbox, view_box, upem): width, height = _get_gradient_units_relative_scale(grad_el, view_box) cx = _number_or_percentage(grad_el.attrib.get("cx", "50%"), width) cy = _number_or_percentage(grad_el.attrib.get("cy", "50%"), height) r = _number_or_percentage(grad_el.attrib.get("r", "50%"), width) raw_fx = grad_el.attrib.get("fx") fx = _number_or_percentage(raw_fx, width) if raw_fx is not None else cx raw_fy = grad_el.attrib.get("fy") fy = _number_or_percentage(raw_fy, height) if raw_fy is not None else cy fr = _number_or_percentage(grad_el.attrib.get("fr", "0%"), width) c0 = Point(fx, fy) r0 = fr c1 = Point(cx, cy) r1 = r transform = _get_gradient_transform(grad_el, shape_bbox, view_box, upem) # The optional Affine2x2 matrix of COLRv1.RadialGradient is used to transform # the circles into ellipses "around their centres": i.e. centres coordinates # are _not_ transformed by it. Thus we apply the full transform to them. c0 = transform.map_point(c0) c1 = transform.map_point(c1) # As for the circle radii (which are affected by Affine2x2), we only scale them # by the maximum of the (absolute) scale or skew. # Then in Affine2x2 we only store a "fraction" of the original transform, i.e. # multiplied by the inverse of the scale that we've already applied to the radii. # Especially when gradientUnits="objectBoundingBox", where circle positions and # radii are expressed using small floats in the range [0..1], this pre-scaling # helps reducing the inevitable rounding errors that arise from storing these # values as integers in COLRv1 tables. s = max(abs(v) for v in transform[:4]) rscale = Affine2D(s, 0, 0, s, 0, 0) r0 = rscale.map_vector((r0, 0)).x r1 = rscale.map_vector((r1, 0)).x affine2x2 = Affine2D.product(rscale.inverse(), transform) gradient = { "c0": c0, "c1": c1, "r0": r0, "r1": r1, "affine2x2": (affine2x2[:4] if affine2x2 != Affine2D.identity() else None), } # TODO handle degenerate cases, fallback to solid, w/e return gradient