417 lines
13 KiB
Python
417 lines
13 KiB
Python
###############################################################################
|
|
#
|
|
# Shape - A class for to represent Excel XLSX shape objects.
|
|
#
|
|
# SPDX-License-Identifier: BSD-2-Clause
|
|
#
|
|
# Copyright (c) 2013-2025, John McNamara, jmcnamara@cpan.org
|
|
#
|
|
import copy
|
|
from warnings import warn
|
|
|
|
|
|
class Shape:
|
|
"""
|
|
A class for to represent Excel XLSX shape objects.
|
|
|
|
|
|
"""
|
|
|
|
###########################################################################
|
|
#
|
|
# Public API.
|
|
#
|
|
###########################################################################
|
|
|
|
def __init__(self, shape_type, name, options):
|
|
"""
|
|
Constructor.
|
|
|
|
"""
|
|
super().__init__()
|
|
self.name = name
|
|
self.shape_type = shape_type
|
|
self.connect = 0
|
|
self.drawing = 0
|
|
self.edit_as = ""
|
|
self.id = 0
|
|
self.text = ""
|
|
self.textlink = ""
|
|
self.stencil = 1
|
|
self.element = -1
|
|
self.start = None
|
|
self.start_index = None
|
|
self.end = None
|
|
self.end_index = None
|
|
self.adjustments = []
|
|
self.start_side = ""
|
|
self.end_side = ""
|
|
self.flip_h = 0
|
|
self.flip_v = 0
|
|
self.rotation = 0
|
|
self.text_rotation = 0
|
|
self.textbox = False
|
|
|
|
self.align = None
|
|
self.fill = None
|
|
self.font = None
|
|
self.format = None
|
|
self.line = None
|
|
self.url_rel_index = None
|
|
self.tip = None
|
|
|
|
self._set_options(options)
|
|
|
|
###########################################################################
|
|
#
|
|
# Private API.
|
|
#
|
|
###########################################################################
|
|
|
|
def _set_options(self, options):
|
|
self.align = self._get_align_properties(options.get("align"))
|
|
self.fill = self._get_fill_properties(options.get("fill"))
|
|
self.font = self._get_font_properties(options.get("font"))
|
|
self.gradient = self._get_gradient_properties(options.get("gradient"))
|
|
self.line = self._get_line_properties(options.get("line"))
|
|
|
|
self.text_rotation = options.get("text_rotation", 0)
|
|
|
|
self.textlink = options.get("textlink", "")
|
|
if self.textlink.startswith("="):
|
|
self.textlink = self.textlink.lstrip("=")
|
|
|
|
if options.get("border"):
|
|
self.line = self._get_line_properties(options["border"])
|
|
|
|
# Gradient fill overrides solid fill.
|
|
if self.gradient:
|
|
self.fill = None
|
|
|
|
###########################################################################
|
|
#
|
|
# Static methods for processing chart/shape style properties.
|
|
#
|
|
###########################################################################
|
|
|
|
@staticmethod
|
|
def _get_line_properties(line):
|
|
# Convert user line properties to the structure required internally.
|
|
|
|
if not line:
|
|
return {"defined": False}
|
|
|
|
# Copy the user defined properties since they will be modified.
|
|
line = copy.deepcopy(line)
|
|
|
|
dash_types = {
|
|
"solid": "solid",
|
|
"round_dot": "sysDot",
|
|
"square_dot": "sysDash",
|
|
"dash": "dash",
|
|
"dash_dot": "dashDot",
|
|
"long_dash": "lgDash",
|
|
"long_dash_dot": "lgDashDot",
|
|
"long_dash_dot_dot": "lgDashDotDot",
|
|
"dot": "dot",
|
|
"system_dash_dot": "sysDashDot",
|
|
"system_dash_dot_dot": "sysDashDotDot",
|
|
}
|
|
|
|
# Check the dash type.
|
|
dash_type = line.get("dash_type")
|
|
|
|
if dash_type is not None:
|
|
if dash_type in dash_types:
|
|
line["dash_type"] = dash_types[dash_type]
|
|
else:
|
|
warn(f"Unknown dash type '{dash_type}'")
|
|
return {}
|
|
|
|
line["defined"] = True
|
|
|
|
return line
|
|
|
|
@staticmethod
|
|
def _get_fill_properties(fill):
|
|
# Convert user fill properties to the structure required internally.
|
|
|
|
if not fill:
|
|
return {"defined": False}
|
|
|
|
# Copy the user defined properties since they will be modified.
|
|
fill = copy.deepcopy(fill)
|
|
|
|
fill["defined"] = True
|
|
|
|
return fill
|
|
|
|
@staticmethod
|
|
def _get_pattern_properties(pattern):
|
|
# Convert user defined pattern to the structure required internally.
|
|
|
|
if not pattern:
|
|
return {}
|
|
|
|
# Copy the user defined properties since they will be modified.
|
|
pattern = copy.deepcopy(pattern)
|
|
|
|
if not pattern.get("pattern"):
|
|
warn("Pattern must include 'pattern'")
|
|
return {}
|
|
|
|
if not pattern.get("fg_color"):
|
|
warn("Pattern must include 'fg_color'")
|
|
return {}
|
|
|
|
types = {
|
|
"percent_5": "pct5",
|
|
"percent_10": "pct10",
|
|
"percent_20": "pct20",
|
|
"percent_25": "pct25",
|
|
"percent_30": "pct30",
|
|
"percent_40": "pct40",
|
|
"percent_50": "pct50",
|
|
"percent_60": "pct60",
|
|
"percent_70": "pct70",
|
|
"percent_75": "pct75",
|
|
"percent_80": "pct80",
|
|
"percent_90": "pct90",
|
|
"light_downward_diagonal": "ltDnDiag",
|
|
"light_upward_diagonal": "ltUpDiag",
|
|
"dark_downward_diagonal": "dkDnDiag",
|
|
"dark_upward_diagonal": "dkUpDiag",
|
|
"wide_downward_diagonal": "wdDnDiag",
|
|
"wide_upward_diagonal": "wdUpDiag",
|
|
"light_vertical": "ltVert",
|
|
"light_horizontal": "ltHorz",
|
|
"narrow_vertical": "narVert",
|
|
"narrow_horizontal": "narHorz",
|
|
"dark_vertical": "dkVert",
|
|
"dark_horizontal": "dkHorz",
|
|
"dashed_downward_diagonal": "dashDnDiag",
|
|
"dashed_upward_diagonal": "dashUpDiag",
|
|
"dashed_horizontal": "dashHorz",
|
|
"dashed_vertical": "dashVert",
|
|
"small_confetti": "smConfetti",
|
|
"large_confetti": "lgConfetti",
|
|
"zigzag": "zigZag",
|
|
"wave": "wave",
|
|
"diagonal_brick": "diagBrick",
|
|
"horizontal_brick": "horzBrick",
|
|
"weave": "weave",
|
|
"plaid": "plaid",
|
|
"divot": "divot",
|
|
"dotted_grid": "dotGrid",
|
|
"dotted_diamond": "dotDmnd",
|
|
"shingle": "shingle",
|
|
"trellis": "trellis",
|
|
"sphere": "sphere",
|
|
"small_grid": "smGrid",
|
|
"large_grid": "lgGrid",
|
|
"small_check": "smCheck",
|
|
"large_check": "lgCheck",
|
|
"outlined_diamond": "openDmnd",
|
|
"solid_diamond": "solidDmnd",
|
|
}
|
|
|
|
# Check for valid types.
|
|
if pattern["pattern"] not in types:
|
|
warn(f"unknown pattern type '{pattern['pattern']}'")
|
|
return {}
|
|
|
|
pattern["pattern"] = types[pattern["pattern"]]
|
|
|
|
# Specify a default background color.
|
|
pattern["bg_color"] = pattern.get("bg_color", "#FFFFFF")
|
|
|
|
return pattern
|
|
|
|
@staticmethod
|
|
def _get_gradient_properties(gradient):
|
|
# pylint: disable=too-many-return-statements
|
|
# Convert user defined gradient to the structure required internally.
|
|
|
|
if not gradient:
|
|
return {}
|
|
|
|
# Copy the user defined properties since they will be modified.
|
|
gradient = copy.deepcopy(gradient)
|
|
|
|
types = {
|
|
"linear": "linear",
|
|
"radial": "circle",
|
|
"rectangular": "rect",
|
|
"path": "shape",
|
|
}
|
|
|
|
# Check the colors array exists and is valid.
|
|
if "colors" not in gradient or not isinstance(gradient["colors"], list):
|
|
warn("Gradient must include colors list")
|
|
return {}
|
|
|
|
# Check the colors array has the required number of entries.
|
|
if not 2 <= len(gradient["colors"]) <= 10:
|
|
warn("Gradient colors list must at least 2 values and not more than 10")
|
|
return {}
|
|
|
|
if "positions" in gradient:
|
|
# Check the positions array has the right number of entries.
|
|
if len(gradient["positions"]) != len(gradient["colors"]):
|
|
warn("Gradient positions not equal to number of colors")
|
|
return {}
|
|
|
|
# Check the positions are in the correct range.
|
|
for pos in gradient["positions"]:
|
|
if not 0 <= pos <= 100:
|
|
warn("Gradient position must be in the range 0 <= position <= 100")
|
|
return {}
|
|
else:
|
|
# Use the default gradient positions.
|
|
if len(gradient["colors"]) == 2:
|
|
gradient["positions"] = [0, 100]
|
|
|
|
elif len(gradient["colors"]) == 3:
|
|
gradient["positions"] = [0, 50, 100]
|
|
|
|
elif len(gradient["colors"]) == 4:
|
|
gradient["positions"] = [0, 33, 66, 100]
|
|
|
|
else:
|
|
warn("Must specify gradient positions")
|
|
return {}
|
|
|
|
angle = gradient.get("angle")
|
|
if angle:
|
|
if not 0 <= angle < 360:
|
|
warn("Gradient angle must be in the range 0 <= angle < 360")
|
|
return {}
|
|
else:
|
|
gradient["angle"] = 90
|
|
|
|
# Check for valid types.
|
|
gradient_type = gradient.get("type")
|
|
|
|
if gradient_type is not None:
|
|
if gradient_type in types:
|
|
gradient["type"] = types[gradient_type]
|
|
else:
|
|
warn(f"Unknown gradient type '{gradient_type}")
|
|
return {}
|
|
else:
|
|
gradient["type"] = "linear"
|
|
|
|
return gradient
|
|
|
|
@staticmethod
|
|
def _get_font_properties(options):
|
|
# Convert user defined font values into private dict values.
|
|
if options is None:
|
|
options = {}
|
|
|
|
font = {
|
|
"name": options.get("name"),
|
|
"color": options.get("color"),
|
|
"size": options.get("size", 11),
|
|
"bold": options.get("bold"),
|
|
"italic": options.get("italic"),
|
|
"underline": options.get("underline"),
|
|
"pitch_family": options.get("pitch_family"),
|
|
"charset": options.get("charset"),
|
|
"baseline": options.get("baseline", -1),
|
|
"lang": options.get("lang", "en-US"),
|
|
}
|
|
|
|
# Convert font size units.
|
|
if font["size"]:
|
|
font["size"] = int(font["size"] * 100)
|
|
|
|
return font
|
|
|
|
@staticmethod
|
|
def _get_font_style_attributes(font):
|
|
# _get_font_style_attributes.
|
|
attributes = []
|
|
|
|
if not font:
|
|
return attributes
|
|
|
|
if font.get("size"):
|
|
attributes.append(("sz", font["size"]))
|
|
|
|
if font.get("bold") is not None:
|
|
attributes.append(("b", 0 + font["bold"]))
|
|
|
|
if font.get("italic") is not None:
|
|
attributes.append(("i", 0 + font["italic"]))
|
|
|
|
if font.get("underline") is not None:
|
|
attributes.append(("u", "sng"))
|
|
|
|
if font.get("baseline") != -1:
|
|
attributes.append(("baseline", font["baseline"]))
|
|
|
|
return attributes
|
|
|
|
@staticmethod
|
|
def _get_font_latin_attributes(font):
|
|
# _get_font_latin_attributes.
|
|
attributes = []
|
|
|
|
if not font:
|
|
return attributes
|
|
|
|
if font["name"] is not None:
|
|
attributes.append(("typeface", font["name"]))
|
|
|
|
if font["pitch_family"] is not None:
|
|
attributes.append(("pitchFamily", font["pitch_family"]))
|
|
|
|
if font["charset"] is not None:
|
|
attributes.append(("charset", font["charset"]))
|
|
|
|
return attributes
|
|
|
|
@staticmethod
|
|
def _get_align_properties(align):
|
|
# Convert user defined align to the structure required internally.
|
|
if not align:
|
|
return {"defined": False}
|
|
|
|
# Copy the user defined properties since they will be modified.
|
|
align = copy.deepcopy(align)
|
|
|
|
if "vertical" in align:
|
|
align_type = align["vertical"]
|
|
|
|
align_types = {
|
|
"top": "top",
|
|
"middle": "middle",
|
|
"bottom": "bottom",
|
|
}
|
|
|
|
if align_type in align_types:
|
|
align["vertical"] = align_types[align_type]
|
|
else:
|
|
warn(f"Unknown alignment type '{align_type}'")
|
|
return {"defined": False}
|
|
|
|
if "horizontal" in align:
|
|
align_type = align["horizontal"]
|
|
|
|
align_types = {
|
|
"left": "left",
|
|
"center": "center",
|
|
"right": "right",
|
|
}
|
|
|
|
if align_type in align_types:
|
|
align["horizontal"] = align_types[align_type]
|
|
else:
|
|
warn(f"Unknown alignment type '{align_type}'")
|
|
return {"defined": False}
|
|
|
|
align["defined"] = True
|
|
|
|
return align
|