# Copyright (c) 2010-2024 fastpyxl
from typing import Any, cast
from fastpyxl.typed_serialisable.base import Serialisable
from fastpyxl.typed_serialisable.errors import FieldValidationError
from fastpyxl.typed_serialisable.fields import AliasField, Field
from fastpyxl.packaging.relationship import (
Relationship,
RelationshipList,
)
from fastpyxl.utils import coordinate_to_tuple
from fastpyxl.utils.units import (
cm_to_EMU,
pixels_to_EMU,
)
from fastpyxl.drawing.image import Image
from fastpyxl.xml.constants import SHEET_DRAWING_NS
from fastpyxl.chart._chart import ChartBase
from .xdr import (
XDRPoint2D,
XDRPositiveSize2D,
)
from .fill import Blip
from .connector import Shape
from .graphic import (
GroupShape,
GraphicFrame,
)
from .geometry import PresetGeometry2D
from .picture import PictureFrame
from .relation import ChartRelation
[docs]
class AnchorClientData(Serialisable):
fLocksWithSheet: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None)
fPrintsWithSheet: bool | None = Field.attribute(expected_type=bool, allow_none=True, default=None)
def __init__(self,
fLocksWithSheet=None,
fPrintsWithSheet=None,
):
self.fLocksWithSheet = fLocksWithSheet
self.fPrintsWithSheet = fPrintsWithSheet
[docs]
class AnchorMarker(Serialisable):
tagname = "marker"
col: int | None = Field.nested_text(expected_type=int, allow_none=True, default=None)
colOff: int | None = Field.nested_text(expected_type=int, allow_none=True, default=None)
row: int | None = Field.nested_text(expected_type=int, allow_none=True, default=None)
rowOff: int | None = Field.nested_text(expected_type=int, allow_none=True, default=None)
def __init__(self,
col=0,
colOff=0,
row=0,
rowOff=0,
):
self.col = col
self.colOff = colOff
self.row = row
self.rowOff = rowOff
class _AnchorBase(Serialisable):
#one of
sp: Shape | None = Field.element(expected_type=Shape, allow_none=True, default=None)
shape = AliasField("sp", default=None)
grpSp: GroupShape | None = Field.element(expected_type=GroupShape, allow_none=True, default=None)
groupShape = AliasField("grpSp", default=None)
graphicFrame: GraphicFrame | None = Field.element(expected_type=GraphicFrame, allow_none=True, default=None)
cxnSp: Shape | None = Field.element(expected_type=Shape, allow_none=True, default=None)
connectionShape = AliasField("cxnSp", default=None)
pic: PictureFrame | None = Field.element(expected_type=PictureFrame, allow_none=True, default=None)
contentPart: str | None = Field.attribute(expected_type=str, allow_none=True, default=None)
clientData: AnchorClientData | None = Field.element(expected_type=AnchorClientData, allow_none=True, default=None)
xml_order = ('sp', 'grpSp', 'graphicFrame', 'cxnSp', 'pic', 'contentPart', 'clientData')
def __init__(self,
clientData=None,
sp=None,
grpSp=None,
graphicFrame=None,
cxnSp=None,
pic=None,
contentPart=None
):
if clientData is None:
clientData = AnchorClientData()
self.clientData = clientData
self.sp = sp
self.grpSp = grpSp
self.graphicFrame = graphicFrame
self.cxnSp = cxnSp
self.pic = pic
self.contentPart = contentPart
[docs]
class AbsoluteAnchor(_AnchorBase):
tagname = "absoluteAnchor"
pos: XDRPoint2D | None = Field.element(expected_type=XDRPoint2D, allow_none=True, default=None)
ext: XDRPositiveSize2D | None = Field.element(expected_type=XDRPositiveSize2D, allow_none=True, default=None)
xml_order = ('pos', 'ext', 'sp', 'grpSp', 'graphicFrame', 'cxnSp', 'pic', 'contentPart', 'clientData')
def __init__(self,
pos=None,
ext=None,
**kw
):
if pos is None:
pos = XDRPoint2D(x=0, y=0)
self.pos = pos
if ext is None:
ext = XDRPositiveSize2D(cx=0, cy=0)
self.ext = ext
super().__init__(**kw)
[docs]
class OneCellAnchor(_AnchorBase):
tagname = "oneCellAnchor"
_from: AnchorMarker | None = Field.element(expected_type=AnchorMarker, allow_none=True, default=None)
ext: XDRPositiveSize2D | None = Field.element(expected_type=XDRPositiveSize2D, allow_none=True, default=None)
xml_order = ('_from', 'ext', 'sp', 'grpSp', 'graphicFrame', 'cxnSp', 'pic', 'contentPart', 'clientData')
def __init__(self,
_from=None,
ext=None,
**kw
):
if _from is None:
_from = AnchorMarker()
self._from = _from
if ext is None:
ext = XDRPositiveSize2D(cx=0, cy=0)
self.ext = ext
super().__init__(**kw)
[docs]
class TwoCellAnchor(_AnchorBase):
tagname = "twoCellAnchor"
editAs: str | None = Field.attribute(
expected_type=str,
allow_none=True,
converter=lambda v: _enum_converter(v, ('twoCell', 'oneCell', 'absolute'), "editAs"), default=None,
)
_from: AnchorMarker | None = Field.element(expected_type=AnchorMarker, allow_none=True, default=None)
to: AnchorMarker | None = Field.element(expected_type=AnchorMarker, allow_none=True, default=None)
xml_order = ('_from', 'to', 'sp', 'grpSp', 'graphicFrame', 'cxnSp', 'pic', 'contentPart', 'clientData')
def __init__(self,
editAs=None,
_from=None,
to=None,
**kw
):
self.editAs = editAs
if _from is None:
_from = AnchorMarker()
self._from = _from
if to is None:
to = AnchorMarker()
self.to = to
super().__init__(**kw)
def _check_anchor(obj):
"""
Check whether an object has an existing Anchor object
If not create a OneCellAnchor using the provided coordinate
"""
anchor = obj.anchor
if not isinstance(anchor, _AnchorBase):
row, col = coordinate_to_tuple(anchor.upper())
anchor = OneCellAnchor()
frm = anchor._from
assert frm is not None
frm.row = row - 1
frm.col = col - 1
ext = anchor.ext
assert ext is not None
if isinstance(obj, ChartBase):
ext.width = cm_to_EMU(obj.width)
ext.height = cm_to_EMU(obj.height)
elif isinstance(obj, Image):
ext.width = pixels_to_EMU(obj.width)
ext.height = pixels_to_EMU(obj.height)
return anchor
[docs]
class SpreadsheetDrawing(Serialisable):
tagname = "wsDr"
mime_type = "application/vnd.openxmlformats-officedocument.drawing+xml"
_rel_type = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing"
_path = PartName="/xl/drawings/drawing{0}.xml"
_id = None
twoCellAnchor: list[TwoCellAnchor] | None = Field.sequence(expected_type=TwoCellAnchor, allow_none=True, default=list)
oneCellAnchor: list[OneCellAnchor] | None = Field.sequence(expected_type=OneCellAnchor, allow_none=True, default=list)
absoluteAnchor: list[AbsoluteAnchor] | None = Field.sequence(expected_type=AbsoluteAnchor, allow_none=True, default=list)
xml_order = ("twoCellAnchor", "oneCellAnchor", "absoluteAnchor")
def __init__(self,
twoCellAnchor=(),
oneCellAnchor=(),
absoluteAnchor=(),
):
self.twoCellAnchor = list(twoCellAnchor)
self.oneCellAnchor = list(oneCellAnchor)
self.absoluteAnchor = list(absoluteAnchor)
self.charts = []
self.images = []
self._rels = []
def __hash__(self):
"""
Just need to check for identity
"""
return id(self)
def __bool__(self):
return bool(self.charts) or bool(self.images)
def _write(self):
"""
create required structure and the serialise
"""
anchors = []
for idx, obj in enumerate(self.charts + self.images, 1):
anchor = _check_anchor(obj)
if isinstance(obj, ChartBase):
rel = Relationship(type="chart", Target=obj.path)
anchor.graphicFrame = self._chart_frame(idx)
elif isinstance(obj, Image):
rel = Relationship(type="image", Target=obj.path)
child = anchor.pic or anchor.groupShape and anchor.groupShape.pic
if not child:
anchor.pic = self._picture_frame(idx)
else:
child.blipFill.blip.embed = "rId{0}".format(idx)
anchors.append(anchor)
self._rels.append(rel)
if self.oneCellAnchor is None:
self.oneCellAnchor = []
if self.twoCellAnchor is None:
self.twoCellAnchor = []
if self.absoluteAnchor is None:
self.absoluteAnchor = []
for a in anchors:
if isinstance(a, OneCellAnchor):
self.oneCellAnchor.append(a)
elif isinstance(a, TwoCellAnchor):
self.twoCellAnchor.append(a)
else:
self.absoluteAnchor.append(a)
tree = self.to_tree()
tree.set('xmlns', SHEET_DRAWING_NS)
return tree
def _chart_frame(self, idx):
chart_rel = ChartRelation(f"rId{idx}")
frame = GraphicFrame()
nv_pr = frame.nvGraphicFramePr
assert nv_pr is not None
nv = nv_pr.cNvPr
assert nv is not None
nv.id = idx
nv.name = "Chart {0}".format(idx)
graphic = frame.graphic
assert graphic is not None
gdata = graphic.graphicData
assert gdata is not None
gdata.chart = chart_rel
return frame
def _picture_frame(self, idx):
pic = PictureFrame()
nv_pic = pic.nvPicPr
assert nv_pic is not None
cnv = nv_pic.cNvPr
assert cnv is not None
cnv.descr = "Picture"
cnv.id = idx
cnv.name = "Image {0}".format(idx)
blip_fill = pic.blipFill
assert blip_fill is not None
blip_fill.blip = Blip()
assert blip_fill.blip is not None
blip_fill.blip.embed = "rId{0}".format(idx)
blip_fill.blip.cstate = "print"
sp_pr = pic.spPr
assert sp_pr is not None
sp_pr.prstGeom = PresetGeometry2D(prst="rect")
sp_pr.ln = None
return pic
def _write_rels(self):
rels = RelationshipList()
for r in self._rels:
rels.append(r)
return rels.to_tree()
@property
def path(self):
return self._path.format(self._id)
@property
def _chart_rels(self):
"""
Get relationship information for each chart and bind anchor to it
"""
rels = []
anchors = (self.absoluteAnchor or []) + (self.oneCellAnchor or []) + (self.twoCellAnchor or [])
for anchor in anchors:
if anchor.graphicFrame is not None:
graphic = anchor.graphicFrame.graphic
if graphic is None:
continue
gd = graphic.graphicData
if gd is None:
continue
rel = gd.chart
if rel is not None:
r = cast(Any, rel)
r.anchor = anchor
r.anchor.graphicFrame = None
rels.append(rel)
return rels
@property
def _blip_rels(self):
"""
Get relationship information for each blip and bind anchor to it
Images that are not part of the XLSX package will be ignored.
"""
rels = []
anchors = (self.absoluteAnchor or []) + (self.oneCellAnchor or []) + (self.twoCellAnchor or [])
for anchor in anchors:
child = anchor.pic or anchor.groupShape and anchor.groupShape.pic
if child and child.blipFill:
rel = child.blipFill.blip
if rel is not None and rel.embed:
rel.anchor = anchor
rels.append(rel)
return rels
def _enum_converter(value, allowed_values, field_name: str):
if value is None:
return None
if value not in allowed_values:
raise FieldValidationError(f"{field_name} rejected value {value!r}")
return value