def populate_root_frame(self, frame, pres_style, draw_style): frame_style = self.style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": pres_style}) parent_frame_style = self.pres.styles.find("office:styles")\ .find({"style:style"}, {"style:name": frame_style["style:parent-style-name"]}) parent_text_props = parent_frame_style.find({"style:text-properties"}, recursive=False) parent_para_props = parent_frame_style.find({"style:paragraph-properties"}, recursive=False) frame_text_style = self.style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": draw_style}) frame_text_props = frame_text_style.find({"style:text-properties"}, recursive=False) frame_para_props = frame_text_style.find({"style:paragraph-properties"}, recursive=False) for prop in TEXT_PROPS: if frame_text_props and prop in frame_text_props.attrs: frame[prop] = frame_text_props[prop] elif parent_text_props and prop in parent_text_props.attrs: frame[prop] = parent_text_props[prop] for prop in PARA_PROPS: if frame_para_props and prop in frame_para_props.attrs: frame[prop] = frame_para_props[prop] elif parent_para_props and prop in parent_para_props.attrs: frame[prop] = parent_para_props[prop] for prop in PARA_PROPS_U: if frame_para_props and prop in frame_para_props.attrs: frame[prop] = units_to_float(frame_para_props[prop]) elif parent_para_props and prop in parent_para_props.attrs: frame[prop] = units_to_float(parent_para_props[prop])
def parse_page_animations(self, timing_root, json_data, page_json_data): main_seq = timing_root.find("anim:seq") click_anims = main_seq.findChildren({"anim:par"}, recursive=False) # Keep track of initially hidden and visible elements that have associated animations init_visible, init_hidden = [], [] for click_anim in click_anims: timed_anims = click_anim.findChildren({"anim:par"}, recursive=False) first_timed_anim = None begin_at = "indefinite" # First node is initiated with a click anim_json_data = {} anim_order = {} for timed_anim in timed_anims: parallel_anims = timed_anim.findChildren({"anim:par"}, recursive=False) begin_delay = units_to_float(timed_anim["smil:begin"]) for anim_data in parallel_anims: # Find animation target anim_subnode = anim_data.findChild() anim_target = "obj_" + anim_subnode["smil:targetelement"] anim_preset = anim_data["presentation:preset-id"] anim_delay = begin_delay + units_to_float(anim_data["smil:begin"]) if anim_target in self.xml_ids: if first_timed_anim: begin_at = first_timed_anim + ".begin+" + str(anim_delay) + "s" anim_id = self.animator.add_animation(\ self, self.xml_ids[anim_target], anim_data, begin_at) if anim_id: if not first_timed_anim: first_timed_anim = anim_id anim_json_data["id"] = anim_id if anim_preset.find("ooo-entrance") == -1\ and anim_target not in init_hidden: init_visible.append(anim_target) elif anim_target not in init_visible and anim_target not in init_hidden: init_hidden.append(anim_target) # Track order of sub-animations if anim_delay in anim_order: anim_order[anim_delay].append(anim_id) else: anim_order[anim_delay] = [anim_id] anim_json_data["anim_order"] = [] for time_index in sorted(anim_order.keys()): anim_json_data["anim_order"] += anim_order[time_index] if "id" in anim_json_data: # Condition prevents "empty" animations begin added (those that refer to # un-implemented types of animation) page_json_data["animations"].append(anim_json_data) page_json_data["init_hidden"] = init_hidden page_json_data["init_visible"] = init_visible
def fill_hatch(cls, dwg, elt, pres, attrs, e_width, e_height, style_tag): hatch_node = pres.styles.find("office:styles")\ .find({"draw:hatch"}, {"draw:name": attrs["draw:fill-hatch-name"]}) hatch_angle = int(hatch_node["draw:rotation"]) / 10 hatch_dist = units_to_float(str(hatch_node["draw:distance"])) # Print background color first pattern = dwg.pattern(insert=(0, 0), size=(e_width, e_height), \ patternUnits="userSpaceOnUse", patternContentUnits="userSpaceOnUse") if style_tag.find( "style:graphic-properties")["draw:fill-hatch-solid"] == "true": pattern.add(dwg.rect((0, 0), (e_width, e_height),\ fill=attrs["draw:fill-color"])) else: pattern.add(dwg.rect((0, 0), (e_width, e_height), fill='#ffffff')) # Single line hatch FillFactory.draw_hatching(dwg, pattern, hatch_angle, hatch_dist,\ hatch_node["draw:color"], 1/DPCM, e_width, e_height) # Double line hatch if hatch_node["draw:style"] in ["double", "triple"]: FillFactory.draw_hatching(dwg, pattern, hatch_angle + 90, hatch_dist,\ hatch_node["draw:color"], 1/DPCM, e_width, e_height) # Triple line hatch if hatch_node["draw:style"] == "triple": FillFactory.draw_hatching(dwg, pattern, hatch_angle + 135, hatch_dist,\ hatch_node["draw:color"], 1/DPCM, e_width, e_height) dwg.defs.add(pattern) elt.fill(pattern.get_paint_server())
def parse_line(self, item, layer_g, style_src): line_x1 = units_to_float(item.attrs["svg:x1"]) line_y1 = units_to_float(item.attrs["svg:y1"]) line_x2 = units_to_float(item.attrs["svg:x2"]) line_y2 = units_to_float(item.attrs["svg:y2"]) line = self.dwg.line(start=(line_x1, line_y1), end=(line_x2, line_y2)) StrokeFactory.stroke(self, self.dwg, item, line, 1, style_src) if "xml:id" in item.attrs: self.xml_ids["obj_"+item["xml:id"]] = { "item": line, "x": min(line_x1, line_x2), "y": min(line_y1, line_y2), "width": abs(line_x2 - line_x1), "height": abs(line_y2 - line_y1) } line.__setitem__("id", "obj_"+item["xml:id"]) layer_g.add(line)
def parse_polygon(self, item, layer_g, style_src): polygon_w = units_to_float(item.attrs["svg:width"]) polygon_h = units_to_float(item.attrs["svg:height"]) polygon_vb = [int(x) for x in item.attrs["svg:viewbox"].split()] # TODO: Check assumption - this is always scaled equally on both axes polygon_pts = item.attrs["draw:points"].replace(',', ' ').split() polygon_d = "M " + ' '.join(polygon_pts[0:2]) + " L " + \ ' '.join(polygon_pts[2:]) + ' Z' # TODO: Add in fill polygon = self.dwg.path(d=polygon_d, fill='none') if "svg:x" in item.attrs and "svg:y" in item.attrs: polygon.translate(units_to_float(item.attrs["svg:x"]),\ units_to_float(item.attrs["svg:y"])) elif "draw:transform" in item.attrs: ShapeParser.transform_shape(item, polygon) polygon_scale = polygon_w / (polygon_vb[2]-polygon_vb[0]) polygon.scale(polygon_scale) style_tag = style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": item["draw:style-name"]}) attrs = style_tag.find("style:graphic-properties").attrs FillFactory.fill(self.dwg, polygon, self, attrs, \ polygon_w/polygon_scale, polygon_h/polygon_scale, style_tag) StrokeFactory.stroke(self, self.dwg, item, polygon, 1/polygon_scale, style_src) if "xml:id" in item.attrs: self.xml_ids["obj_"+item["xml:id"]] = { "item": polygon, "x": units_to_float(item.attrs["svg:x"]), "y": units_to_float(item.attrs["svg:y"]), "width": polygon_w, "height": polygon_h } polygon.__setitem__("id", "obj_"+item["xml:id"]) layer_g.add(polygon)
def parse_path(self, item, layer_g, style_src): path_w = units_to_float(item.attrs["svg:width"]) path_vb = [int(x) for x in item.attrs["svg:viewbox"].split()] # TODO: Check assumption - this is always scaled equally on both axes path = self.dwg.path(d=item.attrs["svg:d"], fill='none') if "svg:x" in item.attrs and "svg:y" in item.attrs: path.translate(units_to_float(item.attrs["svg:x"]),\ units_to_float(item.attrs["svg:y"])) elif "draw:transform" in item.attrs: ShapeParser.transform_shape(item, path) path_scale = path_w / (path_vb[2]-path_vb[0]) path.scale(path_scale) # TODO: See how dashed paths and end markers for paths work StrokeFactory.stroke(self, self.dwg, item, path, 1/path_scale, style_src) if "xml:id" in item.attrs: # TODO: Configure this to work with draw:transforms self.xml_ids["obj_"+item["xml:id"]] = { "item": path, "x": units_to_float(item.attrs["svg:x"]), "y": units_to_float(item.attrs["svg:y"]), "width": path_w, "height": units_to_float(item.attrs["svg:width"]) } path.__setitem__("id", "obj_"+item["xml:id"]) layer_g.add(path)
def __init__(self, url, data_store): self.url = url self.data_store = data_store pres_archive = zipfile.ZipFile(url, 'r') self.styles = BeautifulSoup(pres_archive.read('styles.xml'), \ "lxml", from_encoding='UTF-8') self.content = BeautifulSoup(pres_archive.read('content.xml'), \ "lxml", from_encoding='UTF-8') self.font_mgr = font_manager.FontManager() self.animator = AnimationFactory() self.xml_ids = {} # Create SVG drawing of correct size drawing_size = self.get_document_size() self.d_width = units_to_float(str(drawing_size[0])) self.d_height = units_to_float(str(drawing_size[1])) self.view_box = '0 0 ' + str(self.d_width) + ' ' + str(self.d_height) self.dwg = svgwrite.Drawing(size=drawing_size, viewBox=(self.view_box)) # Setup iterative variables self.clip_id = 0 self.sub_g = 0
def parse_polyline(self, item, layer_g, style_src): polyline_w = units_to_float(item.attrs["svg:width"]) polyline_vb = [int(x) for x in item.attrs["svg:viewbox"].split()] # TODO: Check assumption - this is always scaled equally on both axes polyline_pts = item.attrs["draw:points"].replace(',', ' ').split() polyline_d = "M " + ' '.join(polyline_pts[0:2]) + " L " + ' '.join(polyline_pts[2:]) polyline = self.dwg.path(d=polyline_d, fill='none') if "draw:transform" in item.attrs: ShapeParser.transform_shape(item, polyline) polyline_scale = polyline_w / (polyline_vb[2]-polyline_vb[0]) polyline.scale(polyline_scale) # TODO: See how dashed polylines and end markers for polylines work StrokeFactory.stroke(self, self.dwg, item, polyline, 1/polyline_scale, style_src) if "xml:id" in item.attrs: self.xml_ids["obj_"+item["xml:id"]] = { "item": polyline, "x": units_to_float(item.attrs["svg:x"]), "y": units_to_float(item.attrs["svg:y"]), "width": polyline_w, "height": units_to_float(item.attrs["svg:height"]) } polyline.__setitem__("id", "obj_"+item["xml:id"]) layer_g.add(polyline)
def populate_stack_frame(self, frame, style_name): style_tag = self.style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": style_name}) style_text_props = style_tag.find({"style:text-properties"}, recursive=False) if style_text_props: for prop in TEXT_PROPS: if prop in style_text_props.attrs: frame[prop] = style_text_props[prop] style_para_props = style_tag.find({"style:paragraph-properties"}, recursive=False) if style_para_props: for prop in PARA_PROPS: if prop in style_para_props.attrs: frame[prop] = style_para_props[prop] for prop in PARA_PROPS_U: if prop in style_para_props.attrs: frame[prop] = units_to_float(style_para_props[prop])
def fill_bitmap(cls, dwg, elt, pres, attrs, e_width, e_height): image_node = pres.styles.find("office:styles")\ .find({"draw:fill-image"}, {"draw:name": attrs["draw:fill-image-name"]}) # Extract image to data store pres_archive = zipfile.ZipFile(pres.url, 'r') pres_archive.extract(image_node["xlink:href"], pres.data_store) if "draw:fill-image-width" in attrs: if attrs["draw:fill-image-width"][-1] == "%": bitmap_width = e_width * ( int(attrs["draw:fill-image-width"][:-1]) / 100) bitmap_height = e_height * ( int(attrs["draw:fill-image-height"][:-1]) / 100) else: bitmap_width = units_to_float( str(attrs["draw:fill-image-width"])) bitmap_height = units_to_float( str(attrs["draw:fill-image-height"])) if bitmap_width > 0 and bitmap_height > 0: pattern_size = (bitmap_width, bitmap_height) else: with Image.open(pres.data_store + image_node["xlink:href"]) as img: im_width, im_height = img.size pattern_size = (im_width / DPCM, im_height / DPCM) if attrs["style:repeat"] == "stretch": pattern = dwg.pattern(insert=(0, 0), size=(e_width, e_height), \ patternUnits="userSpaceOnUse", patternContentUnits="userSpaceOnUse") pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=(0, 0), size=(e_width, e_height), preserveAspectRatio="none")) dwg.defs.add(pattern) elt.fill(pattern.get_paint_server()) elif attrs["style:repeat"] == "no-repeat": if attrs["draw:fill-image-ref-point"] in [ "top-left", "left", "bottom-left" ]: image_x = 0 elif attrs["draw:fill-image-ref-point"] in [ "top", "center", "bottom" ]: image_x = (e_width - bitmap_width) / 2 else: image_x = e_width - bitmap_width if attrs["draw:fill-image-ref-point"] in [ "top-left", "top", "top-right" ]: image_y = 0 elif attrs["draw:fill-image-ref-point"] in [ "left", "center", "right" ]: image_y = (e_height - bitmap_height) / 2 else: image_y = e_height - bitmap_height pattern = dwg.pattern(insert=(0, 0), size=(e_width, e_height), \ patternUnits="userSpaceOnUse", patternContentUnits="userSpaceOnUse") pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=(image_x, image_y), size=(bitmap_width, bitmap_height), \ preserveAspectRatio="none")) dwg.defs.add(pattern) elt.fill(pattern.get_paint_server()) else: # Tiled background # Adjust pattern start point based on reference point if attrs["draw:fill-image-ref-point"] in [ "top", "center", "bottom" ]: x_base = (((e_width - pattern_size[0]) / 2) % pattern_size[0]) / pattern_size[0] col_count_parity = (((e_width - pattern_size[0]) / 2) // pattern_size[0] % 2) if col_count_parity == 0: tile_col_offset = [1, 0, 1] else: tile_col_offset = [0, 1, 0] elif attrs["draw:fill-image-ref-point"] in [ "top-right", "right", "bottom-right" ]: x_base = (e_width % pattern_size[0]) / pattern_size[0] col_count_parity = (e_width // pattern_size[0]) % 2 if col_count_parity == 0: tile_col_offset = [0, 1, 0] else: tile_col_offset = [1, 0, 1] else: # top-left, center-left, bottom-left x_base = 0.0 tile_col_offset = [ 1, 0, 1 ] # First full column (index 1) is not offset if attrs["draw:fill-image-ref-point"] in [ "left", "center", "right" ]: y_base = (((e_height - pattern_size[1]) / 2) % pattern_size[1]) / pattern_size[1] row_count_parity = (((e_height - pattern_size[1]) / 2) // pattern_size[1] % 2) if row_count_parity == 0: tile_row_offset = [1, 0, 1] else: tile_row_offset = [0, 1, 0] elif attrs["draw:fill-image-ref-point"] in [ "bottom-left", "bottom", "bottom-right" ]: y_base = (e_height % pattern_size[1]) / pattern_size[1] row_count_parity = (e_height // pattern_size[1]) % 2 if row_count_parity == 0: tile_row_offset = [0, 1, 0] else: tile_row_offset = [1, 0, 1] else: y_base = 0.0 tile_row_offset = [1, 0, 1] # First full row (index 1) is not offset # Adjust pattern offset x_offset = x_base + ( (int(attrs["draw:fill-image-ref-point-x"][:-1]) % 100)) / 100 if x_offset >= 1: x_offset -= 1 if tile_col_offset == [0, 1, 0]: tile_col_offset = [1, 0, 1] else: tile_col_offset = [0, 1, 0] y_offset = y_base + ( (int(attrs["draw:fill-image-ref-point-y"][:-1]) % 100)) / 100 if y_offset >= 1: y_offset -= 1 if tile_row_offset == [0, 1, 0]: tile_row_offset = [1, 0, 1] else: tile_row_offset = [0, 1, 0] # Determine if image has row/col offset or not offset_type = attrs["draw:tile-repeat-offset"].split(" ") if offset_type[0] == "0%": pattern = dwg.pattern(insert=(0, 0), size=pattern_size, \ patternUnits="userSpaceOnUse", patternContentUnits="userSpaceOnUse") pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=(x_offset*pattern_size[0], y_offset*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1)*pattern_size[0], y_offset*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=(x_offset*pattern_size[0], (y_offset-1)*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1)*pattern_size[0], (y_offset-1)*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) else: tiled_offset = (int(offset_type[0][:-1]) % 100) / 100 if offset_type[1] == "horizontal": # Horizontal tiling - make fill 1 wide x 2 high pattern = dwg.pattern(insert=(0, 0), size=(pattern_size[0], 2*pattern_size[1]),\ patternUnits="userSpaceOnUse", patternContentUnits="userSpaceOnUse") if tiled_offset + x_offset >= 1: tiled_offset -= 1 # Top row pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset + tiled_offset*tile_row_offset[0]) * pattern_size[0],\ (y_offset-1)*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1 + tiled_offset*tile_row_offset[0]) * pattern_size[0],\ (y_offset-1)*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) # Centre row pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset + tiled_offset*tile_row_offset[1]) * pattern_size[0],\ y_offset*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1 + tiled_offset*tile_row_offset[1]) * pattern_size[0],\ y_offset*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) # Bottom row pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset + tiled_offset*tile_row_offset[2]) * pattern_size[0],\ (y_offset+1)*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1 + tiled_offset*tile_row_offset[2]) * pattern_size[0],\ (y_offset+1)*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) else: # Vertical tiling - make fill 2 wide x 1 high pattern = dwg.pattern(insert=(0, 0), size=(2*pattern_size[0], pattern_size[1]),\ patternUnits="userSpaceOnUse", patternContentUnits="userSpaceOnUse") if tiled_offset + y_offset >= 1: tiled_offset -= 1 # Left column pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1) * pattern_size[0],\ (y_offset-1 + tiled_offset*tile_col_offset[0]) * pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset-1) * pattern_size[0],\ (y_offset + tiled_offset*tile_col_offset[0])*pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) # Centre column pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=(x_offset * pattern_size[0],\ (y_offset-1 + tiled_offset*tile_col_offset[1]) * pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=(x_offset * pattern_size[0],\ (y_offset + tiled_offset*tile_col_offset[1]) * pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) # Right column pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset+1) * pattern_size[0],\ (y_offset-1 + tiled_offset*tile_col_offset[2]) * pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) pattern.add(dwg.image(pres.data_store + image_node["xlink:href"],\ insert=((x_offset+1) * pattern_size[0],\ (y_offset + tiled_offset*tile_col_offset[2]) * pattern_size[1]),\ size=pattern_size, preserveAspectRatio="none")) dwg.defs.add(pattern) elt.fill(pattern.get_paint_server())
def parse_frame(self, item, layer_g, style_src): # TODO: Work out how to store xml:ids for images with and without borders and text areas # with and without borders... print("parse frame") frame_attrs = item.attrs if "presentation:placeholder" in frame_attrs \ and frame_attrs["presentation:placeholder"] == "true": print("Frame is a placeholder - skip!") return if "draw:transform" in frame_attrs: frame_x, frame_y = 0, 0 else: frame_x = units_to_float(frame_attrs["svg:x"]) frame_y = units_to_float(frame_attrs["svg:y"]) frame_w = units_to_float(frame_attrs["svg:width"]) frame_h = units_to_float(frame_attrs["svg:height"]) # TODO: draw:frame might contain something other than an image... clip_area = None if "draw:style-name" in frame_attrs: style_tag = style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": frame_attrs["draw:style-name"]})\ .find("style:graphic-properties") if style_tag and "fo:clip" in style_tag.attrs: clip_area = style_tag["fo:clip"] if item.find("draw:image"): print("add image") image_href = item.find("draw:image").attrs["xlink:href"] # Extract image to data store pres_archive = zipfile.ZipFile(self.url, 'r') pres_archive.extract(image_href, self.data_store) if clip_area and clip_area[0:4] == "rect": clip = [units_to_float(x) for x in clip_area[5:-1].split(", ")] clip_path = self.dwg.defs.add(\ self.dwg.clipPath(id="clip" + str(self.clip_id))) clip_path.add(self.dwg.rect( insert=(frame_x, frame_y), size=(frame_w, frame_h))) img_px = Image.open(self.data_store + image_href).size img_w = frame_w * img_px[0] / (img_px[0] - DPCM * (clip[1] + clip[3])) img_h = frame_h * img_px[1] / (img_px[1] - DPCM * (clip[0] + clip[2])) img_x = frame_x - \ (frame_w * DPCM * clip[3] / (img_px[0] - DPCM * (clip[1] + clip[3]))) img_y = frame_y - \ (frame_h * DPCM * clip[0] / (img_px[1] - DPCM * (clip[0] + clip[2]))) clip_img = self.dwg.image(self.data_store + image_href,\ insert=(img_x, img_y), size=(img_w, img_h),\ preserveAspectRatio="none",\ clip_path="url(#clip" + str(self.clip_id) + ")") layer_g.add(clip_img) self.clip_id += 1 else: clip_img = self.dwg.image(self.data_store + image_href,\ insert=(frame_x, frame_y), size=(frame_w, frame_h),\ preserveAspectRatio="none") layer_g.add(clip_img) frame = self.dwg.rect(insert=(frame_x, frame_y), \ size=(frame_w, frame_h), fill='none') StrokeFactory.stroke(self, self.dwg, item, frame, 1, style_src) if "draw:transform" in frame_attrs: ShapeParser.transform_shape(item, clip_img) ShapeParser.transform_shape(item, frame) layer_g.add(frame) elif item.find("draw:text-box"): tb_rect = self.dwg.rect(insert=(frame_x, frame_y), size=(frame_w, frame_h)) vert_align = "middle" if "presentation:style-name" in frame_attrs: tb_style_name = frame_attrs["presentation:style-name"] elif "draw:style-name" in frame_attrs: tb_style_name = frame_attrs["draw:style-name"] else: tb_style_name = None if tb_style_name: frame_style = style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": tb_style_name}) frame_graphics = frame_style.find({"style:graphic-properties"}) FillFactory.fill(self.dwg, tb_rect, self, frame_attrs, frame_w, frame_h, \ frame_style) layer_g.add(tb_rect) if "draw:textarea-vertical-align" in frame_graphics.attrs: vert_align = frame_graphics["draw:textarea-vertical-align"] tb_parser = TextBoxParser(self.dwg, self, item, self.font_mgr, vert_align, style_src) tb_parser.visit_textbox(layer_g, "textbox")
def visit_p(self, item_p, is_in_list): # TODO: Underline, overline - final few variations p_stack_frame = self.style_stack[len(self.style_stack) - 1].copy() # shallow copy if "text:style-name" in item_p.attrs: self.populate_stack_frame(p_stack_frame, item_p["text:style-name"]) self.style_stack.append(p_stack_frame) if is_in_list: self.style_stack[-1]["fo:margin-left"] = self.style_stack[-2]["fo:margin-left"] # Add on before spacing to cur_y self.cur_y += p_stack_frame["fo:margin-top"] p_height = 0 first_span = None line_size, line_indent = 0, 0 queued_spans = [] on_first_line = True first_descent = 0 highlights = [] decor_lines = [] for item_span in item_p.find_all({"text:span"}, recursive=False): span_start = 0 span_pos = 0 span_stack_frame = self.style_stack[len(self.style_stack) - 1].copy() # shallow copy if "text:style-name" in item_span.attrs: self.populate_stack_frame(span_stack_frame, item_span["text:style-name"]) self.style_stack.append(span_stack_frame) span_font = self.font_mgr.findfont(FontProperties(\ family=span_stack_frame["style:font-name"], style=span_stack_frame["fo:font-style"], weight=span_stack_frame["fo:font-weight"])) # Text position is either "normal" or a baseline adjustment e.g. "33%" or # a baseline adjustment and font size adjustment e.g. "-33% 58%" # Font size only changes in the final of the three cases if len(span_stack_frame["style:text-position"].split()) == 1: base_font_size = units_to_float(span_stack_frame["fo:font-size"]) else: bf_scale = units_to_float(span_stack_frame["style:text-position"].split()[1]) / 100 base_font_size = units_to_float(span_stack_frame["fo:font-size"]) * bf_scale scaled_font_size = str(base_font_size/DPCM) i_font = ImageFont.truetype(span_font, math.ceil(base_font_size*96/72)) i_font_height = i_font.font.ascent + i_font.font.descent # Replace all <text:s></text:s> tags with spaces for spacer in item_span.find_all({"text:s"}): spacer.replace_with(" ") # Replace all <text:tab></text:tab> tags with literal tabs for tab in item_span.find_all({"text:tab"}): tab.replace_with("\t") for line_break in item_span.find_all({"text:line-break"}): line_break.replace_with("\n") for fields in item_span.find_all({"presentation:date-time", "presentation:footer",\ "text:page-number"}): # TODO: Decide whether/how to implement these properly... fields.replace_with("") print("++" + str(item_span.contents) + "++") span_text = ''.join(item_span.contents) # Cope with line break span if span_text == "\n": line_h, line_d, tspan, h_lights, d_lines = self.process_line(queued_spans, \ [line_indent, span_stack_frame["fo:margin-right"]], \ span_stack_frame["fo:line-height"]) queued_spans.clear() line_size = 0 p_height += line_h if on_first_line: on_first_line = False first_span = tspan first_descent = line_d # Cope with empty or whitespace-only spans (tabs and/or spaces) elif span_text.strip() == "": if span_text == "": span_w = 0 else: span_w = i_font.getsize(span_text)[0] queued_spans.append({\ "style": self.output_tspan_style(span_stack_frame, scaled_font_size), "height": i_font_height/DPCM, "descent": i_font.font.descent/DPCM, "ascent": i_font.font.ascent/DPCM, "width": span_w, "font-size": base_font_size, "text": span_text, "stack-frame": span_stack_frame.copy()}) span_text = "" # Ensure that while loop doesn't run while span_pos != -1: h_lights, d_lines = [], [] # Recalculate available width to take indents into account if on_first_line: line_indent = span_stack_frame["fo:margin-left"] + \ span_stack_frame["fo:text-indent"] else: line_indent = span_stack_frame["fo:margin-left"] line_width = (self.svg_w - line_indent - span_stack_frame["fo:margin-right"])*DPCM # Find next word break next_pos = span_text.find(" ", span_pos+1) if next_pos != -1 and span_text[span_start:next_pos].strip() != "": size = i_font.getsize(span_text[span_start:next_pos]) if size[0] + line_size > line_width: # Write out span up to span_pos then start new span on next line queued_spans.append({\ "style": self.output_tspan_style(span_stack_frame, scaled_font_size), "height": i_font_height/DPCM, "descent": i_font.font.descent/DPCM, "ascent": i_font.font.ascent/DPCM, "width": i_font.getsize(span_text[span_start:span_pos].rstrip())[0], "font-size": base_font_size, "text": span_text[span_start:span_pos].rstrip(), "stack-frame": span_stack_frame.copy()}) line_h, line_d, tspan, h_lights, d_lines = self.process_line(queued_spans, \ [line_indent, span_stack_frame["fo:margin-right"]], \ span_stack_frame["fo:line-height"]) queued_spans.clear() span_start = span_pos line_size = 0 p_height += line_h if on_first_line: on_first_line = False first_span = tspan first_descent = line_d elif next_pos == -1 and span_text[span_start:].strip() != "": # End of span_text has been reached size = i_font.getsize(span_text[span_start:]) if size[0] + line_size > line_width: # Write out span up to span_pos then start new span on next line if span_text[span_start:span_pos] != "": queued_spans.append({\ "style": self.output_tspan_style(span_stack_frame, \ scaled_font_size), "height": i_font_height/DPCM, "descent": i_font.font.descent/DPCM, "ascent": i_font.font.ascent/DPCM, "width": i_font.getsize(span_text[span_start:span_pos]\ .rstrip())[0], "font-size": base_font_size, "text": span_text[span_start:span_pos].rstrip(), "stack-frame": span_stack_frame.copy()}) line_h, line_d, tspan, h_lights, d_lines = self.process_line(queued_spans, \ [line_indent, span_stack_frame["fo:margin-right"]], \ span_stack_frame["fo:line-height"]) queued_spans.clear() # Queue remainder of span queued_spans.append({\ "style": self.output_tspan_style(span_stack_frame, scaled_font_size), "height": i_font_height/DPCM, "descent": i_font.font.descent/DPCM, "ascent": i_font.font.ascent/DPCM, "width": i_font.getsize(span_text[span_pos:])[0], "font-size": base_font_size, "text": span_text[span_pos:], "stack-frame": span_stack_frame.copy()}) line_size = i_font.getsize(span_text[span_pos:])[0] span_start = span_pos p_height += line_h if on_first_line: first_span = tspan on_first_line = False first_descent = line_d else: # Just queue rest of current span queued_spans.append({\ "style": self.output_tspan_style(span_stack_frame, scaled_font_size), "height": i_font_height/DPCM, "descent": i_font.font.descent/DPCM, "ascent": i_font.font.ascent/DPCM, "width": i_font.getsize(span_text[span_start:])[0], "font-size": base_font_size, "text": span_text[span_start:], "stack-frame": span_stack_frame.copy()}) line_size += size[0] span_pos = next_pos # Process highlights and decor_lines for hl in h_lights: highlights.append(hl) for dl in d_lines: decor_lines.append(dl) # Pop stack frame (span) self.style_stack.pop() # Write out any queued spans before going on to next p h_lights, d_lines = [], [] if queued_spans: line_h, line_d, tspan, h_lights, d_lines = self.process_line(queued_spans, \ [line_indent, span_stack_frame["fo:margin-right"]], \ span_stack_frame["fo:line-height"]) queued_spans.clear() p_height += line_h if on_first_line: first_span = tspan first_descent = line_d # Process highlights and decor_lines for hl in h_lights: highlights.append(hl) for dl in d_lines: decor_lines.append(dl) # Add before spacing to first span of paragraph, increase p_height accordingly if first_span: first_dy = first_span.__getitem__("dy") + p_stack_frame["fo:margin-top"] first_span.__setitem__("dy", first_dy) p_height += p_stack_frame["fo:margin-top"] # Add after spacing to cur_y self.cur_y += p_stack_frame["fo:margin-bottom"] # Pop stack frame (p) self.style_stack.pop() return p_height, first_descent, first_span, p_stack_frame["fo:margin-bottom"], \ highlights, decor_lines
def render_shape(cls, dwg, pres, shape, layer, style_src): # TODO: cope with draw:transform for text boxes geom = shape.find("draw:enhanced-geometry") if "svg:viewbox" in geom.attrs: vb_bounds = [int(x) for x in geom["svg:viewbox"].split()] else: vb_bounds = [0, 0, 21600, 21600] if "svg:x" in shape.attrs: base_x = units_to_float(str(shape["svg:x"])) else: base_x = 0.0 if "svg:y" in shape.attrs: base_y = units_to_float(str(shape["svg:y"])) else: base_y = 0.0 bases = [base_x, base_y] scale_x = units_to_float(str(shape["svg:width"])) / \ (vb_bounds[2] - vb_bounds[0]) scale_y = units_to_float(str(shape["svg:height"])) / \ (vb_bounds[3] - vb_bounds[1]) scales = [scale_x, scale_y] if "draw:mirror-horizontal" in geom.attrs and \ geom.attrs["draw:mirror-horizontal"] == "true": x_adj = vb_bounds[2] - vb_bounds[0] else: x_adj = 0 if "draw:mirror-vertical" in geom.attrs and geom.attrs[ "draw:mirror-vertical"] == "true": y_adj = vb_bounds[3] - vb_bounds[1] else: y_adj = 0 adjs = [x_adj, y_adj] # Step 0 - perform substitutions modifiers, eq_results = ShapeParser.evaluate_equations(geom, vb_bounds) # Step 1 - split string into command sections path_sections = re.findall(r'[a-zA-Z][?\$f0-9 -.]*', geom["draw:enhanced-path"]) # Step 2 - translate sections from ODP grammar to SVG equivalent shape_path = dwg.path() path_start_x, path_start_y = 0, 0 cur_x, cur_y = 0, 0 for section in path_sections: # Replace any variables in section section_groups = section.split(" ") for idx, group in enumerate(section_groups): if group and group[0] == "$": section_groups[idx] = str( eval("modifiers[" + group[1:] + "]")) elif group and group[0] == "?": section_groups[idx] = str( eval("eq_results[" + group[2:] + "]")) section = ' '.join(section_groups) # print(section) # Process section if section[0] in ["Z"]: cur_x, cur_y = ShapeParser.closepath\ (section, shape_path, path_start_x, path_start_y) elif section[0] in ["L", "M"]: cur_x, cur_y, path_start_x, path_start_y = ShapeParser.draw_linemove\ (section, shape_path, path_start_x, path_start_y, scales, bases, adjs) elif section[0] in ["C"]: cur_x, cur_y = ShapeParser.draw_curve(section, shape_path, scales, bases, adjs) elif section[0] in ["N"]: # N = endpath print("N not supported") elif section[0] in ["F", "S"]: # F = nofill, S = nostroke print("FS not supported") elif section[0] in ["Q"]: # TODO: Q # Q = quadratic-curveto (x1 y1 x y)+ Q (x1 y1 x y)+ print("Q not yet supported") elif section[0] in ["A", "B", "V", "W"]: cur_x, cur_y, path_start_x, path_start_y = ShapeParser.draw_arc\ (section, shape_path, path_start_x, path_start_y, scales, bases, adjs) elif section[0] in ["T", "U"]: cur_x, cur_y, path_start_x, path_start_y = ShapeParser.draw_ellipse_seg\ (section, shape_path, path_start_x, path_start_y, scales, bases, adjs) elif section[0] in ["X", "Y"]: cur_x, cur_y = ShapeParser.draw_quadrant\ (section, shape_path, cur_x, cur_y, scales, bases, adjs) else: print("Unrecognised command: " + section) # Apply stroke and fill StrokeFactory.stroke(pres, dwg, shape, shape_path, 1, style_src) style_tag = style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": shape["draw:style-name"]}) attrs = style_tag.find("style:graphic-properties").attrs FillFactory.fill(dwg, shape_path, pres, attrs, \ units_to_float(str(shape["svg:width"])), \ units_to_float(str(shape["svg:height"])), style_tag) # Apply transformation to shape if needed if "draw:transform" in shape.attrs: ShapeParser.transform_shape(shape, shape_path) # Store xml:id, if it exists # TODO: Do we need to group the overlaid text with this...? if "xml:id" in shape.attrs: pres.xml_ids["obj_" + shape["xml:id"]] = { "item": shape_path, "x": base_x, "y": base_y, "width": units_to_float(shape["svg:width"]), "height": units_to_float(shape["svg:height"]) } shape_path.__setitem__("id", "obj_" + shape["xml:id"]) # Add custom shape to main drawing layer.add(shape_path) # Overlay any text vert_align = "middle" if "draw:style-name" in shape.attrs: tb_style_name = shape["draw:style-name"] tb_style = style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": tb_style_name}) tb_graphics = tb_style.find({"style:graphic-properties"}) if "draw:textarea-vertical-align" in tb_graphics.attrs: vert_align = tb_graphics["draw:textarea-vertical-align"] tb_parser = TextBoxParser(dwg, pres, shape, pres.font_mgr, vert_align, style_src) tb_parser.visit_textbox(layer, "shape")
def visit_textbox(self, layer, mode): if mode == "textbox": item_tb = self.item.find("draw:text-box") elif mode == "shape": item_tb = self.item self.svg_w = units_to_float(self.item["svg:width"]) self.svg_h = units_to_float(self.item["svg:height"]) if "svg:x" in self.item.attrs: self.svg_x = units_to_float(self.item["svg:x"]) else: self.svg_x = 0.0 if "svg:y" in self.item.attrs: self.svg_y = units_to_float(self.item["svg:y"]) else: self.svg_y = 0.0 # TODO: Now need to transform based on draw:transform if no svg:x,y self.svg_w_px = self.svg_w * DPCM self.cur_y = self.svg_y self.textbox = self.dwg.text('', insert=(self.svg_x, self.svg_y)) # Populate root layer of style stack, keeping track of font family, size and styles # through the text box tree. Each stack frame stores the styles on each layer of # the tree, with missing attributes filled in from parent elements. When going back # up the tree stack frames are popped. self.style_stack = [] root_stack_frame = {"style:font-name": "", "fo:font-size": "",\ "fo:font-style": "normal", "fo:font-weight": "normal", \ "fo:color": "#000000", "fo:text-align": "start",\ "fo:margin-left": 0, "fo:margin-right": 0, "fo:text-indent": 0,\ "fo:margin-top": 0, "fo:margin-bottom": 0, "fo:line-height": 100,\ "style:text-outline": "false", "fo:text-shadow": "none",\ "style:text-position": "normal", "fo:background-color": "transparent", \ "style:text-underline-style": "none", \ "style:text-underline-color": "font-color", \ "style:text-underline-type": "single", \ "style:text-underline-width": "auto", \ "style:text-overline-style": "none",\ "style:text-overline-color": "font-color", \ "style:text-overline-type": "single", \ "style:text-overline-width": "auto", \ "style:text-line-through-style": "none", \ "style:text-line-through-color": "font-color", \ "style:text-line-through-type": "single", \ "style:text-line-through-width": "auto", \ "style:font-relief": "none"} if "presentation:style-name" in self.item.attrs: self.populate_root_frame(root_stack_frame, \ self.item["presentation:style-name"], self.item["draw:text-style-name"]) elif "draw:style-name" in self.item.attrs: self.populate_root_frame(root_stack_frame, \ self.item["draw:style-name"], self.item["draw:text-style-name"]) else: self.populate_stack_frame(root_stack_frame, self.item["draw:text-style-name"]) self.style_stack.append(root_stack_frame) textbox_h = 0 on_first_item = True prev_after = 0 self.highlights = [] self.decor_lines = [] first_span = None first_descent = 0 for p_item in item_tb.find_all({"text:p", "text:list"}, recursive=False): if p_item.name == "text:p": item_h, item_d, tspan, after_i, h_lights, d_lines = self.visit_p(p_item, False) elif p_item.name == "text:list": item_h, item_d, tspan, after_i, h_lights, d_lines = self.visit_list(p_item, 0, None) textbox_h += item_h + after_i if on_first_item: first_span = tspan on_first_item = False first_descent = item_d else: if tspan: item_dy = tspan.__getitem__("dy") + prev_after tspan.__setitem__("dy", item_dy) prev_after = after_i # Process highlights and decor_lines for hl in h_lights: self.highlights.append(hl) for dl in d_lines: self.decor_lines.append(dl) # Set textbox vertical align if self.v_align == "top": v_adj = 0 elif self.v_align == "middle": v_adj = (self.svg_h - textbox_h) / 2 else: v_adj = self.svg_h - textbox_h v_adj -= first_descent # TODO: Get this to work properly - use horizontal lines to check calcs # Doesn't yet appear to work with textboxes that have before-spacing in first paragraph if first_span: textbox_dy = first_span.__getitem__("dy") + v_adj first_span.__setitem__("dy", textbox_dy) for hl in self.highlights: hl["baseline"] += v_adj layer.add(self.dwg.rect( insert=(hl["x"], hl["baseline"] - hl["height"] + hl["descent"]), size=(hl["width"], hl["height"]), fill=hl["color"])) for dl in self.decor_lines: dl["y"] += v_adj stroke_w = dl["font-size"]/(DPCM*12) if dl["line-width"] == "bold": stroke_w *= 2 if dl["decor-type"][3:] == "single": dl_ys = [dl["y"]] else: stroke_w *= 2/3 dl_ys = [dl["y"] - stroke_w, dl["y"] + stroke_w] for y in dl_ys: d_line = self.dwg.line( start=(dl["x"], y), end=(dl["x"] + dl["width"], y), stroke=dl["color"], stroke_width=stroke_w) if dl["style"] in DASH_ARRAYS: d_array = DASH_ARRAYS[dl["style"]] for idx, item in enumerate(d_array): d_array[idx] = item * dl["font-size"] / DPCM d_line.dasharray(d_array) if dl["shadow-color"] != "none": delta_xy = dl["font-size"] / (12*DPCM) s_line = self.dwg.line( start=(dl["x"] + delta_xy, y + delta_xy), end=(dl["x"] + dl["width"] + delta_xy, y + delta_xy), stroke=dl["shadow-color"], stroke_width=stroke_w) if dl["style"] in DASH_ARRAYS: d_array = DASH_ARRAYS[dl["style"]] for idx, item in enumerate(d_array): d_array[idx] = item * dl["font-size"] / DPCM s_line.dasharray(d_array) layer.add(s_line) layer.add(d_line) layer.add(self.textbox)
def visit_list(self, item_l, level, parent_style): # TODO: Bullet image # TODO: Relative indents # Load in list styles if "text:style-name" in item_l.attrs: list_style = item_l["text:style-name"] else: list_style = parent_style l_styles = self.style_src.find("office:automatic-styles")\ .find({"text:list-style"}, {"style:name": list_style}).findChildren(recursive=False) l_height = 0 first_span = None first_descent = 0 on_first_item = True prev_after = 0 highlights = [] decor_lines = [] bullet_font, bullet_color = None, None list_para_style = l_styles[level].find({"style:list-level-properties"}) if "fo:font-family" in l_styles[level].find("style:text-properties").attrs: bullet_font = l_styles[level].find("style:text-properties")["fo:font-family"] if bullet_font == "StarSymbol": bullet_font = "OpenSymbol" # LibreOffice bug 112948 if "fo:color" in l_styles[level].find("style:text-properties").attrs: bullet_color = l_styles[level].find("style:text-properties")["fo:color"] bullet_scale = units_to_float(l_styles[level].find("style:text-properties")\ ["fo:font-size"])/100 list_stack_frame = self.style_stack[len(self.style_stack) - 1].copy() # shallow copy if "text:space-before" in list_para_style.attrs: space_before = units_to_float(list_para_style["text:space-before"]) else: space_before = 0 if "text:min-label-width" in list_para_style.attrs: list_stack_frame["fo:margin-left"] = space_before\ + units_to_float(list_para_style["text:min-label-width"]) else: list_stack_frame["fo:margin-left"] = space_before self.style_stack.append(list_stack_frame) list_header = item_l.find({"text:list-header"}) if level == 0 and list_header: for subheader_item in list_header.find_all({"text:p"}, recursive=False): # TODO: Later add in other children of list-header if subheader_item.name == "text:p": item_h, item_d, tspan, after_i, h_lights, d_lines = self.visit_p(subheader_item, True) l_height += item_h + after_i if on_first_item: first_span = tspan on_first_item = False first_descent = item_d else: if tspan: item_dy = tspan.__getitem__("dy") + prev_after tspan.__setitem__("dy", item_dy) prev_after = after_i # Process highlights and decor_lines for hl in h_lights: highlights.append(hl) for dl in d_lines: decor_lines.append(dl) bullet_count = 0 for list_item in item_l.find_all({"text:list-item"}, recursive=False): for sublist_item in list_item.find_all({"text:p", "text:list"}, recursive=False): if sublist_item.name == "text:list": item_h, item_d, tspan, after_i, h_lights, d_lines = \ self.visit_list(sublist_item, (level+1), list_style) l_height += item_h + after_i else: # Add in bullet point bullet_count += 1 if "text:bullet-char" in l_styles[level].attrs: list_bullet = l_styles[level]["text:bullet-char"] else: # Create numbered bullet based on format specified list_bullet = int_to_format(bullet_count, \ l_styles[level]["style:num-format"]) if "style:num-prefix" in l_styles[level].attrs: list_bullet = l_styles[level]["style:num-prefix"] + list_bullet if "style:num-suffix" in l_styles[level].attrs: list_bullet = list_bullet + l_styles[level]["style:num-suffix"] bullet_span = self.dwg.tspan(list_bullet, x=[self.svg_x+space_before], dy=[0]) self.textbox.add(bullet_span) item_h, item_d, tspan, after_i, h_lights, d_lines = self.visit_p(sublist_item, True) l_height += item_h + after_i if tspan.text.strip() == "": # Remove empty bullet points by deleting bullet text bullet_span.text = "" else: if on_first_item: first_span = bullet_span on_first_item = False first_descent = item_d else: if tspan: item_dy = tspan.__getitem__("dy") + prev_after tspan.__setitem__("dy", item_dy) prev_after = after_i # Copy attributes to bullet_span from tspan bullet_span.__setitem__("dy", tspan.__getitem__("dy")) tspan.__setitem__("dy", 0) bullet_style = tspan.__getitem__("style") if bullet_font: font_start = bullet_style.find("font-family:") font_end = bullet_style.find(";", font_start + 1) bullet_style = bullet_style[:font_start+12] + bullet_font + \ bullet_style[font_end:] size_start = bullet_style.find("font-size:") size_end = bullet_style.find("pt", size_start + 1) scaled_size = float(bullet_style[size_start+10:size_end]) * bullet_scale bullet_style = bullet_style[:size_start+10] + \ str(scaled_size) + bullet_style[size_end:] if bullet_color: color_start = bullet_style.find("fill:") color_end = bullet_style.find(";", color_start + 1) bullet_style = bullet_style[:color_start+5] + bullet_color + \ bullet_style[color_end:] bullet_span.__setitem__("style", bullet_style) # Process highlights and decor_lines for hl in h_lights: highlights.append(hl) for dl in d_lines: decor_lines.append(dl) self.style_stack.pop() # TODO: Get after spacing and bubble up instead of "0" return l_height, first_descent, first_span, 0, highlights, decor_lines
def stroke(cls, pres, dwg, odp_node, svg_elt, scale_factor, style_src): # Get stroke parameters from the style tree of odp_node, using the first encountered # instance of each parameter (i.e. prefer child style over parent style) style_tag = style_src.find("office:automatic-styles")\ .find({"style:style"}, {"style:name": odp_node["draw:style-name"]}) stroke_params = {} continue_descent = True while continue_descent: style_attrs = style_tag.find({"style:graphic-properties"}).attrs for x in ["draw:stroke", "draw:stroke-dash", "svg:stroke-color",\ "svg:stroke-width", "svg:stroke-opacity", \ "draw:marker-start", "draw:marker-start-width", "draw:marker-start-center",\ "draw:marker-end", "draw:marker-end-width", "draw:marker-end-center"]: if x in style_attrs and not x in stroke_params: stroke_params[x] = style_attrs[x] if "style:parent-style-name" in style_tag.attrs: style_tag = pres.styles.find("office:styles")\ .find({"style:style"}, {"style:name": style_tag["style:parent-style-name"]}) else: continue_descent = False if stroke_params["draw:stroke"] in ["solid", "dash"]: stroke_width = units_to_float( str(stroke_params["svg:stroke-width"])) if stroke_width == 0.0: stroke_width = 0.01 stroke_width = stroke_width * scale_factor svg_elt.stroke(stroke_params["svg:stroke-color"]) svg_elt.stroke(width=stroke_width) if "svg:stroke-opacity" in stroke_params: svg_elt.stroke(opacity=int(re.sub(r'[^0-9.]', '', \ str(stroke_params["svg:stroke-opacity"])))/100) else: svg_elt.stroke(width=0) if stroke_params["draw:stroke"] == "dash": dash_node = pres.styles.find("office:styles")\ .find({"draw:stroke-dash"}, {"draw:name": stroke_params["draw:stroke-dash"]}) dash_array = [] gap_length = dash_node["draw:distance"] if gap_length[-1] == "%": gap_length = stroke_width * int(gap_length[:-1]) / 100 else: gap_length = units_to_float(str(gap_length)) * scale_factor if "draw:dots1-length" in dash_node.attrs: dots1_length = dash_node["draw:dots1-length"] else: dots1_length = "100%" if dots1_length[-1] == "%": dots1_length = stroke_width * int(dots1_length[:-1]) / 100 else: dots1_length = units_to_float(str(dots1_length)) * scale_factor for i in range(int(dash_node["draw:dots1"])): dash_array.append(dots1_length) dash_array.append(gap_length) if "draw:dots2" in dash_node.attrs: if "draw:dots2-length" in dash_node.attrs: dots2_length = dash_node["draw:dots2-length"] else: dots2_length = "100%" if dots2_length[-1] == "%": dots2_length = stroke_width * int(dots2_length[:-1]) / 100 else: dots2_length = units_to_float( str(dots2_length)) * scale_factor for j in range(int(dash_node["draw:dots2"])): dash_array.append(dots2_length) dash_array.append(gap_length) # Apply dash array to stroke svg_elt.dasharray(dash_array) # Add start and end markers to lines if odp_node.name in ["draw:line"]: line_angle = math.degrees(math.atan2(\ units_to_float(odp_node.attrs["svg:y2"]) - \ units_to_float(odp_node.attrs["svg:y1"]),\ units_to_float(odp_node.attrs["svg:x2"]) - \ units_to_float(odp_node.attrs["svg:x1"]))) markers = [None, None, None] if "draw:marker-start" in stroke_params: s_marker_style = pres.styles.find("office:styles")\ .find({"draw:marker"}, {"draw:name": stroke_params["draw:marker-start"]}) s_marker_vb = s_marker_style["svg:viewbox"].split() s_marker_w = units_to_float( stroke_params["draw:marker-start-width"]) s_marker_vb_w = int(s_marker_vb[2]) - int(s_marker_vb[0]) s_marker_vb_h = int(s_marker_vb[3]) - int(s_marker_vb[1]) s_marker_d = s_marker_style["svg:d"] s_ref_y = 0.0 if "draw:marker-start-center" in stroke_params: if stroke_params["draw:marker-start-center"] == 'true': s_ref_y = 0.5 s_marker = dwg.marker(insert=(0.5 * s_marker_vb_w, s_ref_y * s_marker_vb_h),\ size=(s_marker_w, s_marker_w * s_marker_vb_h / s_marker_vb_w), \ viewBox=(' '.join(s_marker_vb)), markerUnits="userSpaceOnUse",\ fill=stroke_params["svg:stroke-color"], orient=line_angle-90) s_marker_path = dwg.path(d=s_marker_d) s_marker.add(s_marker_path) dwg.defs.add(s_marker) markers[0] = s_marker if "draw:marker-end" in stroke_params: e_marker_style = pres.styles.find("office:styles")\ .find({"draw:marker"}, {"draw:name": stroke_params["draw:marker-end"]}) e_marker_vb = e_marker_style["svg:viewbox"].split() e_marker_w = units_to_float( stroke_params["draw:marker-end-width"]) e_marker_vb_w = int(e_marker_vb[2]) - int(e_marker_vb[0]) e_marker_vb_h = int(e_marker_vb[3]) - int(e_marker_vb[1]) e_marker_d = e_marker_style["svg:d"] e_ref_y = 0.0 if "draw:marker-end-center" in stroke_params: if stroke_params["draw:marker-end-center"] == 'true': e_ref_y = 0.5 e_marker = dwg.marker(insert=(0.5 * e_marker_vb_w, e_ref_y * e_marker_vb_h),\ size=(e_marker_w, e_marker_w * e_marker_vb_h / e_marker_vb_w), \ viewBox=(' '.join(e_marker_vb)), markerUnits="userSpaceOnUse",\ fill=stroke_params["svg:stroke-color"], orient=line_angle+90) e_marker_path = dwg.path(d=e_marker_d) e_marker.add(e_marker_path) dwg.defs.add(e_marker) markers[2] = e_marker svg_elt.set_markers(tuple(markers))
def process_line(self, queue, margins, line_h): # Create tspan for each span in queue, including any line spacing # Return line_height, first_span (ref to first tspan in line) highlights = [] decor_lines = [] line_height = max(x["height"] for x in queue) * line_h / 100 line_descent = max(x["descent"] for x in queue) line_ascent = max(x["ascent"] for x in queue) self.cur_y += line_height sum_widths = math.fsum(x["width"] for x in queue) available_width = self.svg_w_px - (margins[0] + margins[1]) * DPCM t_align = queue[0]["stack-frame"]["fo:text-align"] if t_align == 'start': first_x = self.svg_x + margins[0] elif t_align == 'center': first_x = (available_width - sum_widths)/(DPCM*2) + self.svg_x + margins[0] elif t_align == 'justify': first_x = self.svg_x + margins[0] else: first_x = (available_width - sum_widths)/DPCM + self.svg_x + margins[0] span_xs = [first_x] # Trim leading and trailing spaces for line queue[0]["text"] = queue[0]["text"].lstrip() queue[-1]["text"] = queue[-1]["text"].rstrip() # Cope with completely blank line by inserting unicode non-breaking space if len(queue) == 1 and queue[0]["text"] == "": queue[0]["text"] = "\xa0" word_lens, space_lens = [0], [0] else: # Split spans into words to allow highlights and decor lines to line up queue_copy = deepcopy(queue) queue.clear() total_words = 0 total_spaces = 0 word_lens = [] space_lens = [] prev_type = "space" for span in queue_copy: span_parts = re.split(r'([ ]+)', span["text"]) span_font = self.font_mgr.findfont(FontProperties(\ family=span["stack-frame"]["style:font-name"], \ style=span["stack-frame"]["fo:font-style"], \ weight=span["stack-frame"]["fo:font-weight"])) i_font = ImageFont.truetype(span_font, math.ceil(float(span["font-size"])*96/72)) for part in span_parts: # Get width of part if part != "": size = i_font.getsize(part) # If word then add to queue and add current value of total width to span_x if part.strip() != "": if prev_type == "word": # Add zero size space space_lens.append(0) queue.append({"text": part, "style": span["style"], \ "stack-frame": span["stack-frame"].copy(), "width": size[0], \ "height": span["height"], "font-size": span["font-size"]}) word_lens.append(size[0]) total_words += size[0] prev_type = "word" else: if prev_type == "space": # Add zero size word queue.append({"text": "\xa0", "style": span["style"], \ "stack-frame": span["stack-frame"].copy(), "width": 0, \ "height": span["height"], "font-size": span["font-size"]}) space_lens.append(size[0]) total_spaces += size[0] prev_type = "space" # Divide available space among spaces if t_align == "justify" and total_spaces > 0: space_scale = (available_width - total_words) / total_spaces else: space_scale = 1 cur_val = 0 if word_lens: for idx, space in enumerate(space_lens): cur_val += (space*space_scale) + word_lens[idx] span_xs.append(cur_val/DPCM + first_x) # Write out queued spans hl_x = first_x prev_highlight = "transparent" prev_ul_str, prev_ul = "none", None prev_ol_str, prev_ol = "none", None prev_lt_str, prev_lt = "none", None for idx, item in enumerate(queue): tspan = self.dwg.tspan(item["text"], style=item["style"]) if idx == 0: first_span = tspan tspan.__setitem__('dy', line_height) tspan.__setitem__('x', span_xs[idx]) bs_adj = 0 if item["stack-frame"]["style:text-position"] != "normal": # Adjust baseline for superscript or subscript text if item["stack-frame"]["style:text-position"].split()[0] == "super": bs_adj = item["height"] * -0.6 elif item["stack-frame"]["style:text-position"].split()[0] == "sub": bs_adj = item["height"] * 0.33 else: bs_adj = item["height"] * \ -units_to_float(item["stack-frame"]["style:text-position"].split()[0])/100 if bs_adj != 0: tspan.__setitem__('dy', bs_adj) self.textbox.add(tspan) if bs_adj != 0: # Reset baseline after superscript or subscript tspan = self.dwg.tspan('\u200B', style=item["style"]) tspan.__setitem__('dy', -float(bs_adj)) self.textbox.add(tspan) # Record highlight if item["stack-frame"]["fo:background-color"] != "transparent": if item["stack-frame"]["fo:background-color"] != prev_highlight: highlights.append({ "baseline": self.cur_y, "x": hl_x, "descent": line_descent, "width": item["width"]/DPCM, "height": line_height, "color": item["stack-frame"]["fo:background-color"]}) prev_highlight = item["stack-frame"]["fo:background-color"] else: highlights[-1]["width"] = (item["width"] /DPCM) + hl_x - highlights[-1]["x"] else: prev_highlight = "transparent" if item["stack-frame"]["fo:text-shadow"] == "none": shadow_color = "none" elif item["stack-frame"]["fo:color"] == "#000000": shadow_color = "#cccccc" else: shadow_color = "#000000" # Record underline if item["stack-frame"]["style:text-underline-style"] != "none": if item["stack-frame"]["style:text-underline-color"] == "font-color": line_color = item["stack-frame"]["fo:color"] else: line_color = item["stack-frame"]["style:text-underline-color"] ul_string = item["stack-frame"]["style:text-underline-style"] + "_" + \ item["stack-frame"]["style:text-underline-width"] + "_" + \ item["stack-frame"]["style:text-underline-type"] + "_" + \ line_color + "_" + shadow_color if ul_string != prev_ul_str: decor_lines.append({ "x": hl_x, "y": self.cur_y + line_descent/2, "width": item["width"]/DPCM, "font-size": item["font-size"], "decor-type": "ul-" + item["stack-frame"]["style:text-underline-type"], "color": line_color, "style": item["stack-frame"]["style:text-underline-style"], "line-width": item["stack-frame"]["style:text-underline-width"], "shadow-color": shadow_color }) prev_ul_str = ul_string prev_ul = decor_lines[-1] else: prev_ul["width"] = (item["width"] /DPCM) + hl_x - prev_ul["x"] else: prev_ul_str = "none" # Record overline if item["stack-frame"]["style:text-overline-style"] != "none": if item["stack-frame"]["style:text-overline-color"] == "font-color": line_color = item["stack-frame"]["fo:color"] else: line_color = item["stack-frame"]["style:text-overline-color"] ol_string = item["stack-frame"]["style:text-overline-style"] + "_" + \ item["stack-frame"]["style:text-overline-width"] + "_" + \ item["stack-frame"]["style:text-overline-type"] + "_" + \ line_color + "_" + shadow_color if ol_string != prev_ol_str: decor_lines.append({ "x": hl_x, "y": self.cur_y - line_ascent + line_descent/2, "width": item["width"]/DPCM, "font-size": item["font-size"], "decor-type": "ol-" + item["stack-frame"]["style:text-overline-type"], "color": line_color, "style": item["stack-frame"]["style:text-overline-style"], "line-width": item["stack-frame"]["style:text-overline-width"], "shadow-color": shadow_color }) prev_ol_str = ol_string prev_ol = decor_lines[-1] else: prev_ol["width"] = (item["width"] /DPCM) + hl_x - prev_ol["x"] else: prev_ol_str = "none" # Record line through if item["stack-frame"]["style:text-line-through-style"] != "none": if item["stack-frame"]["style:text-line-through-color"] == "font-color": line_color = item["stack-frame"]["fo:color"] else: line_color = item["stack-frame"]["style:text-line-through-color"] lt_string = item["stack-frame"]["style:text-line-through-style"] + "_" + \ item["stack-frame"]["style:text-line-through-width"] + "_" + \ item["stack-frame"]["style:text-line-through-type"] + "_" + \ line_color + "_" + shadow_color if lt_string != prev_lt_str: decor_lines.append({ "x": hl_x, "y": self.cur_y + line_descent - (line_height * 0.4), "width": item["width"]/DPCM, "font-size": item["font-size"], "decor-type": "lt-" + item["stack-frame"]["style:text-line-through-type"], "color": line_color, "style": item["stack-frame"]["style:text-line-through-style"], "line-width": item["stack-frame"]["style:text-line-through-width"], "shadow-color": shadow_color }) prev_lt_str = lt_string prev_lt = decor_lines[-1] else: prev_lt["width"] = (item["width"] /DPCM) + hl_x - prev_lt["x"] else: prev_lt_str = "none" if idx < len(space_lens): hl_x += ((item["width"] + space_lens[idx]) / DPCM) return line_height, line_descent, first_span, highlights, decor_lines