383 lines
10 KiB
Python
383 lines
10 KiB
Python
# Copyright (c) 2010-2024 openpyxl
|
|
|
|
from openpyxl.descriptors.serialisable import Serialisable
|
|
from openpyxl.descriptors import (
|
|
Typed,
|
|
Bool,
|
|
NoneSet,
|
|
Integer,
|
|
Sequence,
|
|
Alias,
|
|
)
|
|
from openpyxl.descriptors.nested import (
|
|
NestedText,
|
|
NestedNoneSet,
|
|
)
|
|
from openpyxl.descriptors.excel import Relation
|
|
|
|
from openpyxl.packaging.relationship import (
|
|
Relationship,
|
|
RelationshipList,
|
|
)
|
|
from openpyxl.utils import coordinate_to_tuple
|
|
from openpyxl.utils.units import (
|
|
cm_to_EMU,
|
|
pixels_to_EMU,
|
|
)
|
|
from openpyxl.drawing.image import Image
|
|
|
|
from openpyxl.xml.constants import SHEET_DRAWING_NS
|
|
|
|
from openpyxl.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
|
|
|
|
|
|
class AnchorClientData(Serialisable):
|
|
|
|
fLocksWithSheet = Bool(allow_none=True)
|
|
fPrintsWithSheet = Bool(allow_none=True)
|
|
|
|
def __init__(self,
|
|
fLocksWithSheet=None,
|
|
fPrintsWithSheet=None,
|
|
):
|
|
self.fLocksWithSheet = fLocksWithSheet
|
|
self.fPrintsWithSheet = fPrintsWithSheet
|
|
|
|
|
|
class AnchorMarker(Serialisable):
|
|
|
|
tagname = "marker"
|
|
|
|
col = NestedText(expected_type=int)
|
|
colOff = NestedText(expected_type=int)
|
|
row = NestedText(expected_type=int)
|
|
rowOff = NestedText(expected_type=int)
|
|
|
|
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 = Typed(expected_type=Shape, allow_none=True)
|
|
shape = Alias("sp")
|
|
grpSp = Typed(expected_type=GroupShape, allow_none=True)
|
|
groupShape = Alias("grpSp")
|
|
graphicFrame = Typed(expected_type=GraphicFrame, allow_none=True)
|
|
cxnSp = Typed(expected_type=Shape, allow_none=True)
|
|
connectionShape = Alias("cxnSp")
|
|
pic = Typed(expected_type=PictureFrame, allow_none=True)
|
|
contentPart = Relation()
|
|
|
|
clientData = Typed(expected_type=AnchorClientData)
|
|
|
|
__elements__ = ('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
|
|
|
|
|
|
class AbsoluteAnchor(_AnchorBase):
|
|
|
|
tagname = "absoluteAnchor"
|
|
|
|
pos = Typed(expected_type=XDRPoint2D)
|
|
ext = Typed(expected_type=XDRPositiveSize2D)
|
|
|
|
sp = _AnchorBase.sp
|
|
grpSp = _AnchorBase.grpSp
|
|
graphicFrame = _AnchorBase.graphicFrame
|
|
cxnSp = _AnchorBase.cxnSp
|
|
pic = _AnchorBase.pic
|
|
contentPart = _AnchorBase.contentPart
|
|
clientData = _AnchorBase.clientData
|
|
|
|
__elements__ = ('pos', 'ext') + _AnchorBase.__elements__
|
|
|
|
def __init__(self,
|
|
pos=None,
|
|
ext=None,
|
|
**kw
|
|
):
|
|
if pos is None:
|
|
pos = XDRPoint2D(0, 0)
|
|
self.pos = pos
|
|
if ext is None:
|
|
ext = XDRPositiveSize2D(0, 0)
|
|
self.ext = ext
|
|
super().__init__(**kw)
|
|
|
|
|
|
class OneCellAnchor(_AnchorBase):
|
|
|
|
tagname = "oneCellAnchor"
|
|
|
|
_from = Typed(expected_type=AnchorMarker)
|
|
ext = Typed(expected_type=XDRPositiveSize2D)
|
|
|
|
sp = _AnchorBase.sp
|
|
grpSp = _AnchorBase.grpSp
|
|
graphicFrame = _AnchorBase.graphicFrame
|
|
cxnSp = _AnchorBase.cxnSp
|
|
pic = _AnchorBase.pic
|
|
contentPart = _AnchorBase.contentPart
|
|
clientData = _AnchorBase.clientData
|
|
|
|
__elements__ = ('_from', 'ext') + _AnchorBase.__elements__
|
|
|
|
|
|
def __init__(self,
|
|
_from=None,
|
|
ext=None,
|
|
**kw
|
|
):
|
|
if _from is None:
|
|
_from = AnchorMarker()
|
|
self._from = _from
|
|
if ext is None:
|
|
ext = XDRPositiveSize2D(0, 0)
|
|
self.ext = ext
|
|
super().__init__(**kw)
|
|
|
|
|
|
class TwoCellAnchor(_AnchorBase):
|
|
|
|
tagname = "twoCellAnchor"
|
|
|
|
editAs = NoneSet(values=(['twoCell', 'oneCell', 'absolute']))
|
|
_from = Typed(expected_type=AnchorMarker)
|
|
to = Typed(expected_type=AnchorMarker)
|
|
|
|
sp = _AnchorBase.sp
|
|
grpSp = _AnchorBase.grpSp
|
|
graphicFrame = _AnchorBase.graphicFrame
|
|
cxnSp = _AnchorBase.cxnSp
|
|
pic = _AnchorBase.pic
|
|
contentPart = _AnchorBase.contentPart
|
|
clientData = _AnchorBase.clientData
|
|
|
|
__elements__ = ('_from', 'to') + _AnchorBase.__elements__
|
|
|
|
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()
|
|
anchor._from.row = row -1
|
|
anchor._from.col = col -1
|
|
if isinstance(obj, ChartBase):
|
|
anchor.ext.width = cm_to_EMU(obj.width)
|
|
anchor.ext.height = cm_to_EMU(obj.height)
|
|
elif isinstance(obj, Image):
|
|
anchor.ext.width = pixels_to_EMU(obj.width)
|
|
anchor.ext.height = pixels_to_EMU(obj.height)
|
|
return anchor
|
|
|
|
|
|
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 = Sequence(expected_type=TwoCellAnchor, allow_none=True)
|
|
oneCellAnchor = Sequence(expected_type=OneCellAnchor, allow_none=True)
|
|
absoluteAnchor = Sequence(expected_type=AbsoluteAnchor, allow_none=True)
|
|
|
|
__elements__ = ("twoCellAnchor", "oneCellAnchor", "absoluteAnchor")
|
|
|
|
def __init__(self,
|
|
twoCellAnchor=(),
|
|
oneCellAnchor=(),
|
|
absoluteAnchor=(),
|
|
):
|
|
self.twoCellAnchor = twoCellAnchor
|
|
self.oneCellAnchor = oneCellAnchor
|
|
self.absoluteAnchor = 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)
|
|
|
|
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 = frame.nvGraphicFramePr.cNvPr
|
|
nv.id = idx
|
|
nv.name = "Chart {0}".format(idx)
|
|
frame.graphic.graphicData.chart = chart_rel
|
|
return frame
|
|
|
|
|
|
def _picture_frame(self, idx):
|
|
pic = PictureFrame()
|
|
pic.nvPicPr.cNvPr.descr = "Picture"
|
|
pic.nvPicPr.cNvPr.id = idx
|
|
pic.nvPicPr.cNvPr.name = "Image {0}".format(idx)
|
|
|
|
pic.blipFill.blip = Blip()
|
|
pic.blipFill.blip.embed = "rId{0}".format(idx)
|
|
pic.blipFill.blip.cstate = "print"
|
|
|
|
pic.spPr.prstGeom = PresetGeometry2D(prst="rect")
|
|
pic.spPr.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 + self.oneCellAnchor + self.twoCellAnchor
|
|
for anchor in anchors:
|
|
if anchor.graphicFrame is not None:
|
|
graphic = anchor.graphicFrame.graphic
|
|
rel = graphic.graphicData.chart
|
|
if rel is not None:
|
|
rel.anchor = anchor
|
|
rel.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 + self.oneCellAnchor + self.twoCellAnchor
|
|
|
|
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
|