diff options
Diffstat (limited to '.venv/lib/python3.12/site-packages/pptx/opc')
8 files changed, 1750 insertions, 0 deletions
diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py b/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/__init__.py diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/constants.py b/.venv/lib/python3.12/site-packages/pptx/opc/constants.py new file mode 100644 index 00000000..e1b08a93 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/constants.py @@ -0,0 +1,331 @@ +"""Constant values related to the Open Packaging Convention. + +In particular, this includes content (MIME) types and relationship types. +""" + +from __future__ import annotations + + +class CONTENT_TYPE: + """Content type URIs (like MIME-types) that specify a part's format.""" + + ASF = "video/x-ms-asf" + AVI = "video/avi" + BMP = "image/bmp" + DML_CHART = "application/vnd.openxmlformats-officedocument.drawingml.chart+xml" + DML_CHARTSHAPES = "application/vnd.openxmlformats-officedocument.drawingml.chartshapes+xml" + DML_DIAGRAM_COLORS = "application/vnd.openxmlformats-officedocument.drawingml.diagramColors+xml" + DML_DIAGRAM_DATA = "application/vnd.openxmlformats-officedocument.drawingml.diagramData+xml" + DML_DIAGRAM_DRAWING = "application/vnd.ms-office.drawingml.diagramDrawing+xml" + DML_DIAGRAM_LAYOUT = "application/vnd.openxmlformats-officedocument.drawingml.diagramLayout+xml" + DML_DIAGRAM_STYLE = "application/vnd.openxmlformats-officedocument.drawingml.diagramStyle+xml" + GIF = "image/gif" + INK = "application/inkml+xml" + JPEG = "image/jpeg" + MOV = "video/quicktime" + MP4 = "video/mp4" + MPG = "video/mpeg" + MS_PHOTO = "image/vnd.ms-photo" + MS_VIDEO = "video/msvideo" + OFC_CHART_COLORS = "application/vnd.ms-office.chartcolorstyle+xml" + OFC_CHART_EX = "application/vnd.ms-office.chartex+xml" + OFC_CHART_STYLE = "application/vnd.ms-office.chartstyle+xml" + OFC_CUSTOM_PROPERTIES = "application/vnd.openxmlformats-officedocument.custom-properties+xml" + OFC_CUSTOM_XML_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.customXmlProperties+xml" + ) + OFC_DRAWING = "application/vnd.openxmlformats-officedocument.drawing+xml" + OFC_EXTENDED_PROPERTIES = ( + "application/vnd.openxmlformats-officedocument.extended-properties+xml" + ) + OFC_OLE_OBJECT = "application/vnd.openxmlformats-officedocument.oleObject" + OFC_PACKAGE = "application/vnd.openxmlformats-officedocument.package" + OFC_THEME = "application/vnd.openxmlformats-officedocument.theme+xml" + OFC_THEME_OVERRIDE = "application/vnd.openxmlformats-officedocument.themeOverride+xml" + OFC_VML_DRAWING = "application/vnd.openxmlformats-officedocument.vmlDrawing" + OPC_CORE_PROPERTIES = "application/vnd.openxmlformats-package.core-properties+xml" + OPC_DIGITAL_SIGNATURE_CERTIFICATE = ( + "application/vnd.openxmlformats-package.digital-signature-certificate" + ) + OPC_DIGITAL_SIGNATURE_ORIGIN = "application/vnd.openxmlformats-package.digital-signature-origin" + OPC_DIGITAL_SIGNATURE_XMLSIGNATURE = ( + "application/vnd.openxmlformats-package.digital-signature-xmlsignature+xml" + ) + OPC_RELATIONSHIPS = "application/vnd.openxmlformats-package.relationships+xml" + PML_COMMENTS = "application/vnd.openxmlformats-officedocument.presentationml.comments+xml" + PML_COMMENT_AUTHORS = ( + "application/vnd.openxmlformats-officedocument.presentationml.commentAuthors+xml" + ) + PML_HANDOUT_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.handoutMaster+xml" + ) + PML_NOTES_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.notesMaster+xml" + ) + PML_NOTES_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.notesSlide+xml" + PML_PRESENTATION = "application/vnd.openxmlformats-officedocument.presentationml.presentation" + PML_PRESENTATION_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.presentation.main+xml" + ) + PML_PRES_MACRO_MAIN = "application/vnd.ms-powerpoint.presentation.macroEnabled.main+xml" + PML_PRES_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.presProps+xml" + PML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.presentationml.printerSettings" + ) + PML_SLIDE = "application/vnd.openxmlformats-officedocument.presentationml.slide+xml" + PML_SLIDESHOW_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideshow.main+xml" + ) + PML_SLIDE_LAYOUT = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideLayout+xml" + ) + PML_SLIDE_MASTER = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideMaster+xml" + ) + PML_SLIDE_UPDATE_INFO = ( + "application/vnd.openxmlformats-officedocument.presentationml.slideUpdateInfo+xml" + ) + PML_TABLE_STYLES = ( + "application/vnd.openxmlformats-officedocument.presentationml.tableStyles+xml" + ) + PML_TAGS = "application/vnd.openxmlformats-officedocument.presentationml.tags+xml" + PML_TEMPLATE_MAIN = ( + "application/vnd.openxmlformats-officedocument.presentationml.template.main+xml" + ) + PML_VIEW_PROPS = "application/vnd.openxmlformats-officedocument.presentationml.viewProps+xml" + PNG = "image/png" + SML_CALC_CHAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.calcChain+xml" + SML_CHARTSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.chartsheet+xml" + SML_COMMENTS = "application/vnd.openxmlformats-officedocument.spreadsheetml.comments+xml" + SML_CONNECTIONS = "application/vnd.openxmlformats-officedocument.spreadsheetml.connections+xml" + SML_CUSTOM_PROPERTY = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.customProperty" + ) + SML_DIALOGSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.dialogsheet+xml" + SML_EXTERNAL_LINK = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.externalLink+xml" + ) + SML_PIVOT_CACHE_DEFINITION = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheDefinition+xml" + ) + SML_PIVOT_CACHE_RECORDS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotCacheRecords+xml" + ) + SML_PIVOT_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.pivotTable+xml" + SML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.printerSettings" + ) + SML_QUERY_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.queryTable+xml" + SML_REVISION_HEADERS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionHeaders+xml" + ) + SML_REVISION_LOG = "application/vnd.openxmlformats-officedocument.spreadsheetml.revisionLog+xml" + SML_SHARED_STRINGS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sharedStrings+xml" + ) + SML_SHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + SML_SHEET_MAIN = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet.main+xml" + SML_SHEET_METADATA = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheetMetadata+xml" + ) + SML_STYLES = "application/vnd.openxmlformats-officedocument.spreadsheetml.styles+xml" + SML_TABLE = "application/vnd.openxmlformats-officedocument.spreadsheetml.table+xml" + SML_TABLE_SINGLE_CELLS = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.tableSingleCells+xml" + ) + SML_TEMPLATE_MAIN = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.template.main+xml" + ) + SML_USER_NAMES = "application/vnd.openxmlformats-officedocument.spreadsheetml.userNames+xml" + SML_VOLATILE_DEPENDENCIES = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.volatileDependencies+xml" + ) + SML_WORKSHEET = "application/vnd.openxmlformats-officedocument.spreadsheetml.worksheet+xml" + SWF = "application/x-shockwave-flash" + TIFF = "image/tiff" + VIDEO = "video/unknown" + WML_COMMENTS = "application/vnd.openxmlformats-officedocument.wordprocessingml.comments+xml" + WML_DOCUMENT = "application/vnd.openxmlformats-officedocument.wordprocessingml.document" + WML_DOCUMENT_GLOSSARY = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.glossary+xml" + ) + WML_DOCUMENT_MAIN = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml" + ) + WML_ENDNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.endnotes+xml" + WML_FONT_TABLE = "application/vnd.openxmlformats-officedocument.wordprocessingml.fontTable+xml" + WML_FOOTER = "application/vnd.openxmlformats-officedocument.wordprocessingml.footer+xml" + WML_FOOTNOTES = "application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml" + WML_HEADER = "application/vnd.openxmlformats-officedocument.wordprocessingml.header+xml" + WML_NUMBERING = "application/vnd.openxmlformats-officedocument.wordprocessingml.numbering+xml" + WML_PRINTER_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.printerSettings" + ) + WML_SETTINGS = "application/vnd.openxmlformats-officedocument.wordprocessingml.settings+xml" + WML_STYLES = "application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml" + WML_WEB_SETTINGS = ( + "application/vnd.openxmlformats-officedocument.wordprocessingml.webSettings+xml" + ) + WMV = "video/x-ms-wmv" + XML = "application/xml" + X_EMF = "image/x-emf" + X_FONTDATA = "application/x-fontdata" + X_FONT_TTF = "application/x-font-ttf" + X_MS_VIDEO = "video/x-msvideo" + X_WMF = "image/x-wmf" + + +class NAMESPACE: + """Constant values for OPC XML namespaces""" + + DML_WORDPROCESSING_DRAWING = ( + "http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing" + ) + OFC_RELATIONSHIPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships" + OPC_RELATIONSHIPS = "http://schemas.openxmlformats.org/package/2006/relationships" + OPC_CONTENT_TYPES = "http://schemas.openxmlformats.org/package/2006/content-types" + WML_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main" + + +class RELATIONSHIP_TARGET_MODE: + """Open XML relationship target modes""" + + EXTERNAL = "External" + INTERNAL = "Internal" + + +class RELATIONSHIP_TYPE: + AUDIO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/audio" + A_F_CHUNK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/aFChunk" + CALC_CHAIN = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/calcChain" + CERTIFICATE = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" + "re/certificate" + ) + CHART = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chart" + CHARTSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartsheet" + CHART_COLOR_STYLE = "http://schemas.microsoft.com/office/2011/relationships/chartColorStyle" + CHART_USER_SHAPES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/chartUserShapes" + ) + COMMENTS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/comments" + COMMENT_AUTHORS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/commentAuthors" + ) + CONNECTIONS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/connections" + CONTROL = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/control" + CORE_PROPERTIES = ( + "http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" + ) + CUSTOM_PROPERTIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/custom-properties" + ) + CUSTOM_PROPERTY = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customProperty" + ) + CUSTOM_XML = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXml" + CUSTOM_XML_PROPS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/customXmlProps" + ) + DIAGRAM_COLORS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramColors" + ) + DIAGRAM_DATA = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramData" + DIAGRAM_LAYOUT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramLayout" + ) + DIAGRAM_QUICK_STYLE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/diagramQuickStyle" + ) + DIALOGSHEET = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/dialogsheet" + DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/drawing" + ENDNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/endnotes" + EXTENDED_PROPERTIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/extended-properties" + ) + EXTERNAL_LINK = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/externalLink" + ) + FONT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/font" + FONT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/fontTable" + FOOTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footer" + FOOTNOTES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes" + GLOSSARY_DOCUMENT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/glossaryDocument" + ) + HANDOUT_MASTER = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/handoutMaster" + ) + HEADER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/header" + HYPERLINK = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink" + IMAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image" + MEDIA = "http://schemas.microsoft.com/office/2007/relationships/media" + NOTES_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesMaster" + NOTES_SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/notesSlide" + NUMBERING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/numbering" + OFFICE_DOCUMENT = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" + ) + OLE_OBJECT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" + ORIGIN = "http://schemas.openxmlformats.org/package/2006/relationships/digital-signature/origin" + PACKAGE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/package" + PIVOT_CACHE_DEFINITION = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotCac" + "heDefinition" + ) + PIVOT_CACHE_RECORDS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/spreadsh" + "eetml/pivotCacheRecords" + ) + PIVOT_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/pivotTable" + PRES_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/presProps" + PRINTER_SETTINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/printerSettings" + ) + QUERY_TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/queryTable" + REVISION_HEADERS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionHeaders" + ) + REVISION_LOG = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/revisionLog" + SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/settings" + SHARED_STRINGS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sharedStrings" + ) + SHEET_METADATA = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/sheetMetadata" + ) + SIGNATURE = ( + "http://schemas.openxmlformats.org/package/2006/relationships/digital-signatu" + "re/signature" + ) + SLIDE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slide" + SLIDE_LAYOUT = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideLayout" + SLIDE_MASTER = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideMaster" + SLIDE_UPDATE_INFO = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/slideUpdateInfo" + ) + STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" + TABLE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/table" + TABLE_SINGLE_CELLS = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableSingleCells" + ) + TABLE_STYLES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tableStyles" + TAGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/tags" + THEME = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/theme" + THEME_OVERRIDE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/themeOverride" + ) + THUMBNAIL = "http://schemas.openxmlformats.org/package/2006/relationships/metadata/thumbnail" + USERNAMES = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/usernames" + VIDEO = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/video" + VIEW_PROPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/viewProps" + VML_DRAWING = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/vmlDrawing" + VOLATILE_DEPENDENCIES = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/volatile" + "Dependencies" + ) + WEB_SETTINGS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/webSettings" + WORKSHEET_SOURCE = ( + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/worksheetSource" + ) + XML_MAPS = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/xmlMaps" diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py b/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py new file mode 100644 index 00000000..5dd902a5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/oxml.py @@ -0,0 +1,188 @@ +"""OPC-local oxml module to handle OPC-local concerns like relationship parsing.""" + +from __future__ import annotations + +from typing import TYPE_CHECKING, Callable, cast + +from lxml import etree + +from pptx.opc.constants import NAMESPACE as NS +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.oxml import parse_xml, register_element_cls +from pptx.oxml.simpletypes import ( + ST_ContentType, + ST_Extension, + ST_TargetMode, + XsdAnyUri, + XsdId, +) +from pptx.oxml.xmlchemy import ( + BaseOxmlElement, + OptionalAttribute, + RequiredAttribute, + ZeroOrMore, +) + +if TYPE_CHECKING: + from pptx.opc.packuri import PackURI + +nsmap = { + "ct": NS.OPC_CONTENT_TYPES, + "pr": NS.OPC_RELATIONSHIPS, + "r": NS.OFC_RELATIONSHIPS, +} + + +def oxml_to_encoded_bytes( + element: BaseOxmlElement, + encoding: str = "utf-8", + pretty_print: bool = False, + standalone: bool | None = None, +) -> bytes: + return etree.tostring( + element, encoding=encoding, pretty_print=pretty_print, standalone=standalone + ) + + +def oxml_tostring( + elm: BaseOxmlElement, + encoding: str | None = None, + pretty_print: bool = False, + standalone: bool | None = None, +): + return etree.tostring(elm, encoding=encoding, pretty_print=pretty_print, standalone=standalone) + + +def serialize_part_xml(part_elm: BaseOxmlElement) -> bytes: + """Produce XML-file bytes for `part_elm`, suitable for writing directly to a `.xml` file. + + Includes XML-declaration header. + """ + return etree.tostring(part_elm, encoding="UTF-8", standalone=True) + + +class CT_Default(BaseOxmlElement): + """`<Default>` element. + + Specifies the default content type to be applied to a part with the specified extension. + """ + + extension: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Extension", ST_Extension + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) + + +class CT_Override(BaseOxmlElement): + """`<Override>` element. + + Specifies the content type to be applied for a part with the specified partname. + """ + + partName: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "PartName", XsdAnyUri + ) + contentType: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "ContentType", ST_ContentType + ) + + +class CT_Relationship(BaseOxmlElement): + """`<Relationship>` element. + + Represents a single relationship from a source to a target part. + """ + + rId: str = RequiredAttribute("Id", XsdId) # pyright: ignore[reportAssignmentType] + reltype: str = RequiredAttribute("Type", XsdAnyUri) # pyright: ignore[reportAssignmentType] + target_ref: str = RequiredAttribute( # pyright: ignore[reportAssignmentType] + "Target", XsdAnyUri + ) + targetMode: str = OptionalAttribute( # pyright: ignore[reportAssignmentType] + "TargetMode", ST_TargetMode, default=RTM.INTERNAL + ) + + @classmethod + def new( + cls, rId: str, reltype: str, target_ref: str, target_mode: str = RTM.INTERNAL + ) -> CT_Relationship: + """Return a new `<Relationship>` element. + + `target_ref` is either a partname or a URI. + """ + relationship = cast(CT_Relationship, parse_xml(f'<Relationship xmlns="{nsmap["pr"]}"/>')) + relationship.rId = rId + relationship.reltype = reltype + relationship.target_ref = target_ref + relationship.targetMode = target_mode + return relationship + + +class CT_Relationships(BaseOxmlElement): + """`<Relationships>` element, the root element in a .rels file.""" + + relationship_lst: list[CT_Relationship] + _insert_relationship: Callable[[CT_Relationship], CT_Relationship] + + relationship = ZeroOrMore("pr:Relationship") + + def add_rel( + self, rId: str, reltype: str, target: str, is_external: bool = False + ) -> CT_Relationship: + """Add a child `<Relationship>` element with attributes set as specified.""" + target_mode = RTM.EXTERNAL if is_external else RTM.INTERNAL + relationship = CT_Relationship.new(rId, reltype, target, target_mode) + return self._insert_relationship(relationship) + + @classmethod + def new(cls) -> CT_Relationships: + """Return a new `<Relationships>` element.""" + return cast(CT_Relationships, parse_xml(f'<Relationships xmlns="{nsmap["pr"]}"/>')) + + @property + def xml_file_bytes(self) -> bytes: + """Return XML bytes, with XML-declaration, for this `<Relationships>` element. + + Suitable for saving in a .rels stream, not pretty printed and with an XML declaration at + the top. + """ + return oxml_to_encoded_bytes(self, encoding="UTF-8", standalone=True) + + +class CT_Types(BaseOxmlElement): + """`<Types>` element. + + The container element for Default and Override elements in [Content_Types].xml. + """ + + default_lst: list[CT_Default] + override_lst: list[CT_Override] + + _add_default: Callable[..., CT_Default] + _add_override: Callable[..., CT_Override] + + default = ZeroOrMore("ct:Default") + override = ZeroOrMore("ct:Override") + + def add_default(self, ext: str, content_type: str) -> CT_Default: + """Add a child `<Default>` element with attributes set to parameter values.""" + return self._add_default(extension=ext, contentType=content_type) + + def add_override(self, partname: PackURI, content_type: str) -> CT_Override: + """Add a child `<Override>` element with attributes set to parameter values.""" + return self._add_override(partName=partname, contentType=content_type) + + @classmethod + def new(cls) -> CT_Types: + """Return a new `<Types>` element.""" + return cast(CT_Types, parse_xml(f'<Types xmlns="{nsmap["ct"]}"/>')) + + +register_element_cls("ct:Default", CT_Default) +register_element_cls("ct:Override", CT_Override) +register_element_cls("ct:Types", CT_Types) + +register_element_cls("pr:Relationship", CT_Relationship) +register_element_cls("pr:Relationships", CT_Relationships) diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/package.py b/.venv/lib/python3.12/site-packages/pptx/opc/package.py new file mode 100644 index 00000000..713759c5 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/package.py @@ -0,0 +1,762 @@ +"""Fundamental Open Packaging Convention (OPC) objects. + +The :mod:`pptx.packaging` module coheres around the concerns of reading and writing +presentations to and from a .pptx file. +""" + +from __future__ import annotations + +import collections +from typing import IO, TYPE_CHECKING, DefaultDict, Iterator, Mapping, Set, cast + +from pptx.opc.constants import RELATIONSHIP_TARGET_MODE as RTM +from pptx.opc.constants import RELATIONSHIP_TYPE as RT +from pptx.opc.oxml import CT_Relationships, serialize_part_xml +from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI +from pptx.opc.serialized import PackageReader, PackageWriter +from pptx.opc.shared import CaseInsensitiveDict +from pptx.oxml import parse_xml +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from typing_extensions import Self + + from pptx.opc.oxml import CT_Relationship, CT_Types + from pptx.oxml.xmlchemy import BaseOxmlElement + from pptx.package import Package + from pptx.parts.presentation import PresentationPart + + +class _RelatableMixin: + """Provide relationship methods required by both the package and each part.""" + + def part_related_by(self, reltype: str) -> Part: + """Return (single) part having relationship to this package of `reltype`. + + Raises |KeyError| if no such relationship is found and |ValueError| if more than one such + relationship is found. + """ + return self._rels.part_with_reltype(reltype) + + def relate_to(self, target: Part | str, reltype: str, is_external: bool = False) -> str: + """Return rId key of relationship of `reltype` to `target`. + + If such a relationship already exists, its rId is returned. Otherwise the relationship is + added and its new rId returned. + """ + if isinstance(target, str): + assert is_external + return self._rels.get_or_add_ext_rel(reltype, target) + + return self._rels.get_or_add(reltype, target) + + def related_part(self, rId: str) -> Part: + """Return related |Part| subtype identified by `rId`.""" + return self._rels[rId].target_part + + def target_ref(self, rId: str) -> str: + """Return URL contained in target ref of relationship identified by `rId`.""" + return self._rels[rId].target_ref + + @lazyproperty + def _rels(self) -> _Relationships: + """|_Relationships| object containing relationships from this part to others.""" + raise NotImplementedError( # pragma: no cover + "`%s` must implement `.rels`" % type(self).__name__ + ) + + +class OpcPackage(_RelatableMixin): + """Main API class for |python-opc|. + + A new instance is constructed by calling the :meth:`open` classmethod with a path to a package + file or file-like object containing a package (.pptx file). + """ + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + @classmethod + def open(cls, pkg_file: str | IO[bytes]) -> Self: + """Return an |OpcPackage| instance loaded with the contents of `pkg_file`.""" + return cls(pkg_file)._load() + + def drop_rel(self, rId: str) -> None: + """Remove relationship identified by `rId`.""" + self._rels.pop(rId) + + def iter_parts(self) -> Iterator[Part]: + """Generate exactly one reference to each part in the package.""" + visited: Set[Part] = set() + for rel in self.iter_rels(): + if rel.is_external: + continue + part = rel.target_part + if part in visited: + continue + yield part + visited.add(part) + + def iter_rels(self) -> Iterator[_Relationship]: + """Generate exactly one reference to each relationship in package. + + Performs a depth-first traversal of the rels graph. + """ + visited: Set[Part] = set() + + def walk_rels(rels: _Relationships) -> Iterator[_Relationship]: + for rel in rels.values(): + yield rel + # --- external items can have no relationships --- + if rel.is_external: + continue + # -- all relationships other than those for the package belong to a part. Once + # -- that part has been processed, processing it again would lead to the same + # -- relationships appearing more than once. + part = rel.target_part + if part in visited: + continue + visited.add(part) + # --- recurse into relationships of each unvisited target-part --- + yield from walk_rels(part.rels) + + yield from walk_rels(self._rels) + + @property + def main_document_part(self) -> PresentationPart: + """Return |Part| subtype serving as the main document part for this package. + + In this case it will be a |Presentation| part. + """ + return cast("PresentationPart", self.part_related_by(RT.OFFICE_DOCUMENT)) + + def next_partname(self, tmpl: str) -> PackURI: + """Return |PackURI| next available partname matching `tmpl`. + + `tmpl` is a printf (%)-style template string containing a single replacement item, a '%d' + to be used to insert the integer portion of the partname. Example: + '/ppt/slides/slide%d.xml' + """ + # --- expected next partname is tmpl % n where n is one greater than the number + # --- of existing partnames that match tmpl. Speed up finding the next one + # --- (maybe) by searching from the end downward rather than from 1 upward. + prefix = tmpl[: (tmpl % 42).find("42")] + partnames = {p.partname for p in self.iter_parts() if p.partname.startswith(prefix)} + for n in range(len(partnames) + 1, 0, -1): + candidate_partname = tmpl % n + if candidate_partname not in partnames: + return PackURI(candidate_partname) + raise Exception("ProgrammingError: ran out of candidate_partnames") # pragma: no cover + + def save(self, pkg_file: str | IO[bytes]) -> None: + """Save this package to `pkg_file`. + + `file` can be either a path to a file (a string) or a file-like object. + """ + PackageWriter.write(pkg_file, self._rels, tuple(self.iter_parts())) + + def _load(self) -> Self: + """Return the package after loading all parts and relationships.""" + pkg_xml_rels, parts = _PackageLoader.load(self._pkg_file, cast("Package", self)) + self._rels.load_from_xml(PACKAGE_URI, pkg_xml_rels, parts) + return self + + @lazyproperty + def _rels(self) -> _Relationships: + """|Relationships| object containing relationships of this package.""" + return _Relationships(PACKAGE_URI.baseURI) + + +class _PackageLoader: + """Function-object that loads a package from disk (or other store).""" + + def __init__(self, pkg_file: str | IO[bytes], package: Package): + self._pkg_file = pkg_file + self._package = package + + @classmethod + def load( + cls, pkg_file: str | IO[bytes], package: Package + ) -> tuple[CT_Relationships, dict[PackURI, Part]]: + """Return (pkg_xml_rels, parts) pair resulting from loading `pkg_file`. + + The returned `parts` value is a {partname: part} mapping with each part in the package + included and constructed complete with its relationships to other parts in the package. + + The returned `pkg_xml_rels` value is a `CT_Relationships` object containing the parsed + package relationships. It is the caller's responsibility (the package object) to load + those relationships into its |_Relationships| object. + """ + return cls(pkg_file, package)._load() + + def _load(self) -> tuple[CT_Relationships, dict[PackURI, Part]]: + """Return (pkg_xml_rels, parts) pair resulting from loading pkg_file.""" + parts, xml_rels = self._parts, self._xml_rels + + for partname, part in parts.items(): + part.load_rels_from_xml(xml_rels[partname], parts) + + return xml_rels[PACKAGE_URI], parts + + @lazyproperty + def _content_types(self) -> _ContentTypeMap: + """|_ContentTypeMap| object providing content-types for items of this package. + + Provides a content-type (MIME-type) for any given partname. + """ + return _ContentTypeMap.from_xml(self._package_reader[CONTENT_TYPES_URI]) + + @lazyproperty + def _package_reader(self) -> PackageReader: + """|PackageReader| object providing access to package-items in pkg_file.""" + return PackageReader(self._pkg_file) + + @lazyproperty + def _parts(self) -> dict[PackURI, Part]: + """dict {partname: Part} populated with parts loading from package. + + Among other duties, this collection is passed to each relationships collection so each + relationship can resolve a reference to its target part when required. This reference can + only be reliably carried out once the all parts have been loaded. + """ + content_types = self._content_types + package = self._package + package_reader = self._package_reader + + return { + partname: PartFactory( + partname, + content_types[partname], + package, + blob=package_reader[partname], + ) + for partname in (p for p in self._xml_rels if p != "/") + # -- invalid partnames can arise in some packages; ignore those rather than raise an + # -- exception. + if partname in package_reader + } + + @lazyproperty + def _xml_rels(self) -> dict[PackURI, CT_Relationships]: + """dict {partname: xml_rels} for package and all package parts. + + This is used as the basis for other loading operations such as loading parts and + populating their relationships. + """ + xml_rels: dict[PackURI, CT_Relationships] = {} + visited_partnames: Set[PackURI] = set() + + def load_rels(source_partname: PackURI, rels: CT_Relationships): + """Populate `xml_rels` dict by traversing relationships depth-first.""" + xml_rels[source_partname] = rels + visited_partnames.add(source_partname) + base_uri = source_partname.baseURI + + # --- recursion stops when there are no unvisited partnames in rels --- + for rel in rels.relationship_lst: + if rel.targetMode == RTM.EXTERNAL: + continue + target_partname = PackURI.from_rel_ref(base_uri, rel.target_ref) + if target_partname in visited_partnames: + continue + load_rels(target_partname, self._xml_rels_for(target_partname)) + + load_rels(PACKAGE_URI, self._xml_rels_for(PACKAGE_URI)) + return xml_rels + + def _xml_rels_for(self, partname: PackURI) -> CT_Relationships: + """Return CT_Relationships object formed by parsing rels XML for `partname`. + + A CT_Relationships object is returned in all cases. A part that has no relationships + receives an "empty" CT_Relationships object, i.e. containing no `CT_Relationship` objects. + """ + rels_xml = self._package_reader.rels_xml_for(partname) + return ( + CT_Relationships.new() + if rels_xml is None + else cast(CT_Relationships, parse_xml(rels_xml)) + ) + + +class Part(_RelatableMixin): + """Base class for package parts. + + Provides common properties and methods, but intended to be subclassed in client code to + implement specific part behaviors. Also serves as the default class for parts that are not yet + given specific behaviors. + """ + + def __init__( + self, partname: PackURI, content_type: str, package: Package, blob: bytes | None = None + ): + # --- XmlPart subtypes, don't store a blob (the original XML) --- + self._partname = partname + self._content_type = content_type + self._package = package + self._blob = blob + + @classmethod + def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Self: + """Return `cls` instance loaded from arguments. + + This one is a straight pass-through, but subtypes may do some pre-processing, see XmlPart + for an example. + """ + return cls(partname, content_type, package, blob) + + @property + def blob(self) -> bytes: + """Contents of this package part as a sequence of bytes. + + Intended to be overridden by subclasses. Default behavior is to return the blob initial + loaded during `Package.open()` operation. + """ + return self._blob or b"" + + @blob.setter + def blob(self, blob: bytes): + """Note that not all subclasses use the part blob as their blob source. + + In particular, the |XmlPart| subclass uses its `self._element` to serialize a blob on + demand. This works fine for binary parts though. + """ + self._blob = blob + + @lazyproperty + def content_type(self) -> str: + """Content-type (MIME-type) of this part.""" + return self._content_type + + def load_rels_from_xml(self, xml_rels: CT_Relationships, parts: dict[PackURI, Part]) -> None: + """load _Relationships for this part from `xml_rels`. + + Part references are resolved using the `parts` dict that maps each partname to the loaded + part with that partname. These relationships are loaded from a serialized package and so + already have assigned rIds. This method is only used during package loading. + """ + self._rels.load_from_xml(self._partname.baseURI, xml_rels, parts) + + @lazyproperty + def package(self) -> Package: + """Package this part belongs to.""" + return self._package + + @property + def partname(self) -> PackURI: + """|PackURI| partname for this part, e.g. "/ppt/slides/slide1.xml".""" + return self._partname + + @partname.setter + def partname(self, partname: PackURI): + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( # pragma: no cover + "partname must be instance of PackURI, got '%s'" % type(partname).__name__ + ) + self._partname = partname + + @lazyproperty + def rels(self) -> _Relationships: + """Collection of relationships from this part to other parts.""" + # --- this must be public to allow the part graph to be traversed --- + return self._rels + + def _blob_from_file(self, file: str | IO[bytes]) -> bytes: + """Return bytes of `file`, which is either a str path or a file-like object.""" + # --- a str `file` is assumed to be a path --- + if isinstance(file, str): + with open(file, "rb") as f: + return f.read() + + # --- otherwise, assume `file` is a file-like object + # --- reposition file cursor if it has one + if callable(getattr(file, "seek")): + file.seek(0) + return file.read() + + @lazyproperty + def _rels(self) -> _Relationships: + """Relationships from this part to others.""" + return _Relationships(self._partname.baseURI) + + +class XmlPart(Part): + """Base class for package parts containing an XML payload, which is most of them. + + Provides additional methods to the |Part| base class that take care of parsing and + reserializing the XML payload and managing relationships to other parts. + """ + + def __init__( + self, partname: PackURI, content_type: str, package: Package, element: BaseOxmlElement + ): + super(XmlPart, self).__init__(partname, content_type, package) + self._element = element + + @classmethod + def load(cls, partname: PackURI, content_type: str, package: Package, blob: bytes): + """Return instance of `cls` loaded with parsed XML from `blob`.""" + return cls( + partname, content_type, package, element=cast("BaseOxmlElement", parse_xml(blob)) + ) + + @property + def blob(self) -> bytes: # pyright: ignore[reportIncompatibleMethodOverride] + """bytes XML serialization of this part.""" + return serialize_part_xml(self._element) + + # -- XmlPart cannot set its blob, which is why pyright complains -- + + def drop_rel(self, rId: str) -> None: + """Remove relationship identified by `rId` if its reference count is under 2. + + Relationships with a reference count of 0 are implicit relationships. Note that only XML + parts can drop relationships. + """ + if self._rel_ref_count(rId) < 2: + self._rels.pop(rId) + + @property + def part(self): + """This part. + + This is part of the parent protocol, "children" of the document will not know the part + that contains them so must ask their parent object. That chain of delegation ends here for + child objects. + """ + return self + + def _rel_ref_count(self, rId: str) -> int: + """Return int count of references in this part's XML to `rId`.""" + return len([r for r in cast("list[str]", self._element.xpath("//@r:id")) if r == rId]) + + +class PartFactory: + """Constructs a registered subtype of |Part|. + + Client code can register a subclass of |Part| to be used for a package blob based on its + content type. + """ + + part_type_for: dict[str, type[Part]] = {} + + def __new__(cls, partname: PackURI, content_type: str, package: Package, blob: bytes) -> Part: + PartClass = cls._part_cls_for(content_type) + return PartClass.load(partname, content_type, package, blob) + + @classmethod + def _part_cls_for(cls, content_type: str) -> type[Part]: + """Return the custom part class registered for `content_type`. + + Returns |Part| if no custom class is registered for `content_type`. + """ + if content_type in cls.part_type_for: + return cls.part_type_for[content_type] + return Part + + +class _ContentTypeMap: + """Value type providing dict semantics for looking up content type by partname.""" + + def __init__(self, overrides: dict[str, str], defaults: dict[str, str]): + self._overrides = overrides + self._defaults = defaults + + def __getitem__(self, partname: PackURI) -> str: + """Return content-type (MIME-type) for part identified by *partname*.""" + if not isinstance(partname, PackURI): # pyright: ignore[reportUnnecessaryIsInstance] + raise TypeError( + "_ContentTypeMap key must be <type 'PackURI'>, got %s" % type(partname).__name__ + ) + + if partname in self._overrides: + return self._overrides[partname] + + if partname.ext in self._defaults: + return self._defaults[partname.ext] + + raise KeyError("no content-type for partname '%s' in [Content_Types].xml" % partname) + + @classmethod + def from_xml(cls, content_types_xml: bytes) -> _ContentTypeMap: + """Return |_ContentTypeMap| instance populated from `content_types_xml`.""" + types_elm = cast("CT_Types", parse_xml(content_types_xml)) + # -- note all partnames in [Content_Types].xml are absolute -- + overrides = CaseInsensitiveDict( + (o.partName.lower(), o.contentType) for o in types_elm.override_lst + ) + defaults = CaseInsensitiveDict( + (d.extension.lower(), d.contentType) for d in types_elm.default_lst + ) + return cls(overrides, defaults) + + +class _Relationships(Mapping[str, "_Relationship"]): + """Collection of |_Relationship| instances having `dict` semantics. + + Relationships are keyed by their rId, but may also be found in other ways, such as by their + relationship type. |Relationship| objects are keyed by their rId. + + Iterating this collection has normal mapping semantics, generating the keys (rIds) of the + mapping. `rels.keys()`, `rels.values()`, and `rels.items() can be used as they would be for a + `dict`. + """ + + def __init__(self, base_uri: str): + self._base_uri = base_uri + + def __contains__(self, rId: object) -> bool: + """Implement 'in' operation, like `"rId7" in relationships`.""" + return rId in self._rels + + def __getitem__(self, rId: str) -> _Relationship: + """Implement relationship lookup by rId using indexed access, like rels[rId].""" + try: + return self._rels[rId] + except KeyError: + raise KeyError("no relationship with key '%s'" % rId) + + def __iter__(self) -> Iterator[str]: + """Implement iteration of rIds (iterating a mapping produces its keys).""" + return iter(self._rels) + + def __len__(self) -> int: + """Return count of relationships in collection.""" + return len(self._rels) + + def get_or_add(self, reltype: str, target_part: Part) -> str: + """Return str rId of `reltype` to `target_part`. + + The rId of an existing matching relationship is used if present. Otherwise, a new + relationship is added and that rId is returned. + """ + existing_rId = self._get_matching(reltype, target_part) + return ( + self._add_relationship(reltype, target_part) if existing_rId is None else existing_rId + ) + + def get_or_add_ext_rel(self, reltype: str, target_ref: str) -> str: + """Return str rId of external relationship of `reltype` to `target_ref`. + + The rId of an existing matching relationship is used if present. Otherwise, a new + relationship is added and that rId is returned. + """ + existing_rId = self._get_matching(reltype, target_ref, is_external=True) + return ( + self._add_relationship(reltype, target_ref, is_external=True) + if existing_rId is None + else existing_rId + ) + + def load_from_xml( + self, base_uri: str, xml_rels: CT_Relationships, parts: dict[PackURI, Part] + ) -> None: + """Replace any relationships in this collection with those from `xml_rels`.""" + + def iter_valid_rels(): + """Filter out broken relationships such as those pointing to NULL.""" + for rel_elm in xml_rels.relationship_lst: + # --- Occasionally a PowerPoint plugin or other client will "remove" + # --- a relationship simply by "voiding" its Target value, like making + # --- it "/ppt/slides/NULL". Skip any relationships linking to a + # --- partname that is not present in the package. + if rel_elm.targetMode == RTM.INTERNAL: + partname = PackURI.from_rel_ref(base_uri, rel_elm.target_ref) + if partname not in parts: + continue + yield _Relationship.from_xml(base_uri, rel_elm, parts) + + self._rels.clear() + self._rels.update((rel.rId, rel) for rel in iter_valid_rels()) + + def part_with_reltype(self, reltype: str) -> Part: + """Return target part of relationship with matching `reltype`. + + Raises |KeyError| if not found and |ValueError| if more than one matching relationship is + found. + """ + rels_of_reltype = self._rels_by_reltype[reltype] + + if len(rels_of_reltype) == 0: + raise KeyError("no relationship of type '%s' in collection" % reltype) + + if len(rels_of_reltype) > 1: + raise ValueError("multiple relationships of type '%s' in collection" % reltype) + + return rels_of_reltype[0].target_part + + def pop(self, rId: str) -> _Relationship: + """Return |_Relationship| identified by `rId` after removing it from collection. + + The caller is responsible for ensuring it is no longer required. + """ + return self._rels.pop(rId) + + @property + def xml(self): + """bytes XML serialization of this relationship collection. + + This value is suitable for storage as a .rels file in an OPC package. Includes a `<?xml..` + declaration header with encoding as UTF-8. + """ + rels_elm = CT_Relationships.new() + + # -- Sequence <Relationship> elements deterministically (in numerical order) to + # -- simplify testing and manual inspection. + def iter_rels_in_numerical_order(): + sorted_num_rId_pairs = sorted( + ( + int(rId[3:]) if rId.startswith("rId") and rId[3:].isdigit() else 0, + rId, + ) + for rId in self.keys() + ) + return (self[rId] for _, rId in sorted_num_rId_pairs) + + for rel in iter_rels_in_numerical_order(): + rels_elm.add_rel(rel.rId, rel.reltype, rel.target_ref, rel.is_external) + + return rels_elm.xml_file_bytes + + def _add_relationship(self, reltype: str, target: Part | str, is_external: bool = False) -> str: + """Return str rId of |_Relationship| newly added to spec.""" + rId = self._next_rId + self._rels[rId] = _Relationship( + self._base_uri, + rId, + reltype, + target_mode=RTM.EXTERNAL if is_external else RTM.INTERNAL, + target=target, + ) + return rId + + def _get_matching( + self, reltype: str, target: Part | str, is_external: bool = False + ) -> str | None: + """Return optional str rId of rel of `reltype`, `target`, and `is_external`. + + Returns `None` on no matching relationship + """ + for rel in self._rels_by_reltype[reltype]: + if rel.is_external != is_external: + continue + rel_target = rel.target_ref if rel.is_external else rel.target_part + if rel_target == target: + return rel.rId + + return None + + @property + def _next_rId(self) -> str: + """Next str rId available in collection. + + The next rId is the first unused key starting from "rId1" and making use of any gaps in + numbering, e.g. 'rId2' for rIds ['rId1', 'rId3']. + """ + # --- The common case is where all sequential numbers starting at "rId1" are + # --- used and the next available rId is "rId%d" % (len(rels)+1). So we start + # --- there and count down to produce the best performance. + for n in range(len(self) + 1, 0, -1): + rId_candidate = "rId%d" % n # like 'rId19' + if rId_candidate not in self._rels: + return rId_candidate + raise Exception( + "ProgrammingError: Impossible to have more distinct rIds than relationships" + ) + + @lazyproperty + def _rels(self) -> dict[str, _Relationship]: + """dict {rId: _Relationship} containing relationships of this collection.""" + return {} + + @property + def _rels_by_reltype(self) -> dict[str, list[_Relationship]]: + """defaultdict {reltype: [rels]} for all relationships in collection.""" + D: DefaultDict[str, list[_Relationship]] = collections.defaultdict(list) + for rel in self.values(): + D[rel.reltype].append(rel) + return D + + +class _Relationship: + """Value object describing link from a part or package to another part.""" + + def __init__(self, base_uri: str, rId: str, reltype: str, target_mode: str, target: Part | str): + self._base_uri = base_uri + self._rId = rId + self._reltype = reltype + self._target_mode = target_mode + self._target = target + + @classmethod + def from_xml( + cls, base_uri: str, rel: CT_Relationship, parts: dict[PackURI, Part] + ) -> _Relationship: + """Return |_Relationship| object based on CT_Relationship element `rel`.""" + target = ( + rel.target_ref + if rel.targetMode == RTM.EXTERNAL + else parts[PackURI.from_rel_ref(base_uri, rel.target_ref)] + ) + return cls(base_uri, rel.rId, rel.reltype, rel.targetMode, target) + + @lazyproperty + def is_external(self) -> bool: + """True if target_mode is `RTM.EXTERNAL`. + + An external relationship is a link to a resource outside the package, such as a + web-resource (URL). + """ + return self._target_mode == RTM.EXTERNAL + + @lazyproperty + def reltype(self) -> str: + """Member of RELATIONSHIP_TYPE describing relationship of target to source.""" + return self._reltype + + @lazyproperty + def rId(self) -> str: + """str relationship-id, like 'rId9'. + + Corresponds to the `Id` attribute on the `CT_Relationship` element and uniquely identifies + this relationship within its peers for the source-part or package. + """ + return self._rId + + @lazyproperty + def target_part(self) -> Part: + """|Part| or subtype referred to by this relationship.""" + if self.is_external: + raise ValueError( + "`.target_part` property on _Relationship is undefined when " + "target-mode is external" + ) + assert isinstance(self._target, Part) + return self._target + + @lazyproperty + def target_partname(self) -> PackURI: + """|PackURI| instance containing partname targeted by this relationship. + + Raises `ValueError` on reference if target_mode is external. Use :attr:`target_mode` to + check before referencing. + """ + if self.is_external: + raise ValueError( + "`.target_partname` property on _Relationship is undefined when " + "target-mode is external" + ) + assert isinstance(self._target, Part) + return self._target.partname + + @lazyproperty + def target_ref(self) -> str: + """str reference to relationship target. + + For internal relationships this is the relative partname, suitable for serialization + purposes. For an external relationship it is typically a URL. + """ + if self.is_external: + assert isinstance(self._target, str) + return self._target + + return self.target_partname.relative_ref(self._base_uri) diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py b/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py new file mode 100644 index 00000000..74ddd333 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/packuri.py @@ -0,0 +1,109 @@ +"""Provides the PackURI value type and known pack-URI strings such as PACKAGE_URI.""" + +from __future__ import annotations + +import posixpath +import re + + +class PackURI(str): + """Proxy for a pack URI (partname). + + Provides utility properties the baseURI and the filename slice. Behaves as |str| otherwise. + """ + + _filename_re = re.compile("([a-zA-Z]+)([0-9][0-9]*)?") + + def __new__(cls, pack_uri_str: str): + if not pack_uri_str[0] == "/": + raise ValueError(f"PackURI must begin with slash, got {repr(pack_uri_str)}") + return str.__new__(cls, pack_uri_str) + + @staticmethod + def from_rel_ref(baseURI: str, relative_ref: str) -> PackURI: + """Construct an absolute pack URI formed by translating `relative_ref` onto `baseURI`.""" + joined_uri = posixpath.join(baseURI, relative_ref) + abs_uri = posixpath.abspath(joined_uri) + return PackURI(abs_uri) + + @property + def baseURI(self) -> str: + """The base URI of this pack URI; the directory portion, roughly speaking. + + E.g. `"/ppt/slides"` for `"/ppt/slides/slide1.xml"`. + + For the package pseudo-partname "/", the baseURI is "/". + """ + return posixpath.split(self)[0] + + @property + def ext(self) -> str: + """The extension portion of this pack URI. + + E.g. `"xml"` for `"/ppt/slides/slide1.xml"`. Note the leading period is not included. + """ + # -- raw_ext is either empty string or starts with period, e.g. ".xml" -- + raw_ext = posixpath.splitext(self)[1] + return raw_ext[1:] if raw_ext.startswith(".") else raw_ext + + @property + def filename(self) -> str: + """The "filename" portion of this pack URI. + + E.g. `"slide1.xml"` for `"/ppt/slides/slide1.xml"`. + + For the package pseudo-partname "/", `filename` is ''. + """ + return posixpath.split(self)[1] + + @property + def idx(self) -> int | None: + """Optional int partname index. + + Value is an integer for an "array" partname or None for singleton partname, e.g. `21` for + `"/ppt/slides/slide21.xml"` and |None| for `"/ppt/presentation.xml"`. + """ + filename = self.filename + if not filename: + return None + name_part = posixpath.splitext(filename)[0] # filename w/ext removed + match = self._filename_re.match(name_part) + if match is None: + return None + if match.group(2): + return int(match.group(2)) + return None + + @property + def membername(self) -> str: + """The pack URI with the leading slash stripped off. + + This is the form used as the Zip file membername for the package item. Returns "" for the + package pseudo-partname "/". + """ + return self[1:] + + def relative_ref(self, baseURI: str) -> str: + """Return string containing relative reference to package item from `baseURI`. + + E.g. PackURI("/ppt/slideLayouts/slideLayout1.xml") would return + "../slideLayouts/slideLayout1.xml" for baseURI "/ppt/slides". + """ + # workaround for posixpath bug in 2.6, doesn't generate correct + # relative path when `start` (second) parameter is root ("/") + return self[1:] if baseURI == "/" else posixpath.relpath(self, baseURI) + + @property + def rels_uri(self) -> PackURI: + """The pack URI of the .rels part corresponding to the current pack URI. + + Only produces sensible output if the pack URI is a partname or the package pseudo-partname + "/". + """ + rels_filename = "%s.rels" % self.filename + rels_uri_str = posixpath.join(self.baseURI, "_rels", rels_filename) + return PackURI(rels_uri_str) + + +PACKAGE_URI = PackURI("/") +CONTENT_TYPES_URI = PackURI("/[Content_Types].xml") diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py b/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py new file mode 100644 index 00000000..92366708 --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/serialized.py @@ -0,0 +1,296 @@ +"""API for reading/writing serialized Open Packaging Convention (OPC) package.""" + +from __future__ import annotations + +import os +import posixpath +import zipfile +from typing import IO, TYPE_CHECKING, Any, Container, Sequence + +from pptx.exc import PackageNotFoundError +from pptx.opc.constants import CONTENT_TYPE as CT +from pptx.opc.oxml import CT_Types, serialize_part_xml +from pptx.opc.packuri import CONTENT_TYPES_URI, PACKAGE_URI, PackURI +from pptx.opc.shared import CaseInsensitiveDict +from pptx.opc.spec import default_content_types +from pptx.util import lazyproperty + +if TYPE_CHECKING: + from pptx.opc.package import Part, _Relationships # pyright: ignore[reportPrivateUsage] + + +class PackageReader(Container[bytes]): + """Provides access to package-parts of an OPC package with dict semantics. + + The package may be in zip-format (a .pptx file) or expanded into a directory structure, + perhaps by unzipping a .pptx file. + """ + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + def __contains__(self, pack_uri: object) -> bool: + """Return True when part identified by `pack_uri` is present in package.""" + return pack_uri in self._blob_reader + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Return bytes for part corresponding to `pack_uri`.""" + return self._blob_reader[pack_uri] + + def rels_xml_for(self, partname: PackURI) -> bytes | None: + """Return optional rels item XML for `partname`. + + Returns `None` if no rels item is present for `partname`. `partname` is a |PackURI| + instance. + """ + blob_reader, uri = self._blob_reader, partname.rels_uri + return blob_reader[uri] if uri in blob_reader else None + + @lazyproperty + def _blob_reader(self) -> _PhysPkgReader: + """|_PhysPkgReader| subtype providing read access to the package file.""" + return _PhysPkgReader.factory(self._pkg_file) + + +class PackageWriter: + """Writes a zip-format OPC package to `pkg_file`. + + `pkg_file` can be either a path to a zip file (a string) or a file-like object. `pkg_rels` is + the |_Relationships| object containing relationships for the package. `parts` is a sequence of + |Part| subtype instance to be written to the package. + + Its single API classmethod is :meth:`write`. This class is not intended to be instantiated. + """ + + def __init__(self, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part]): + self._pkg_file = pkg_file + self._pkg_rels = pkg_rels + self._parts = parts + + @classmethod + def write( + cls, pkg_file: str | IO[bytes], pkg_rels: _Relationships, parts: Sequence[Part] + ) -> None: + """Write a physical package (.pptx file) to `pkg_file`. + + The serialized package contains `pkg_rels` and `parts`, a content-types stream based on + the content type of each part, and a .rels file for each part that has relationships. + """ + cls(pkg_file, pkg_rels, parts)._write() + + def _write(self) -> None: + """Write physical package (.pptx file).""" + with _PhysPkgWriter.factory(self._pkg_file) as phys_writer: + self._write_content_types_stream(phys_writer) + self._write_pkg_rels(phys_writer) + self._write_parts(phys_writer) + + def _write_content_types_stream(self, phys_writer: _PhysPkgWriter) -> None: + """Write `[Content_Types].xml` part to the physical package. + + This part must contain an appropriate content type lookup target for each part in the + package. + """ + phys_writer.write( + CONTENT_TYPES_URI, + serialize_part_xml(_ContentTypesItem.xml_for(self._parts)), + ) + + def _write_parts(self, phys_writer: _PhysPkgWriter) -> None: + """Write blob of each part in `parts` to the package. + + A rels item for each part is also written when the part has relationships. + """ + for part in self._parts: + phys_writer.write(part.partname, part.blob) + if part._rels: # pyright: ignore[reportPrivateUsage] + phys_writer.write(part.partname.rels_uri, part.rels.xml) + + def _write_pkg_rels(self, phys_writer: _PhysPkgWriter) -> None: + """Write the XML rels item for `pkg_rels` ('/_rels/.rels') to the package.""" + phys_writer.write(PACKAGE_URI.rels_uri, self._pkg_rels.xml) + + +class _PhysPkgReader(Container[PackURI]): + """Base class for physical package reader objects.""" + + def __contains__(self, item: object) -> bool: + """Must be implemented by each subclass.""" + raise NotImplementedError( # pragma: no cover + "`%s` must implement `.__contains__()`" % type(self).__name__ + ) + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Blob for part corresponding to `pack_uri`.""" + raise NotImplementedError( # pragma: no cover + f"`{type(self).__name__}` must implement `.__contains__()`" + ) + + @classmethod + def factory(cls, pkg_file: str | IO[bytes]) -> _PhysPkgReader: + """Return |_PhysPkgReader| subtype instance appropriage for `pkg_file`.""" + # --- for pkg_file other than str, assume it's a stream and pass it to Zip + # --- reader to sort out + if not isinstance(pkg_file, str): + return _ZipPkgReader(pkg_file) + + # --- otherwise we treat `pkg_file` as a path --- + if os.path.isdir(pkg_file): + return _DirPkgReader(pkg_file) + + if zipfile.is_zipfile(pkg_file): + return _ZipPkgReader(pkg_file) + + raise PackageNotFoundError("Package not found at '%s'" % pkg_file) + + +class _DirPkgReader(_PhysPkgReader): + """Implements |PhysPkgReader| interface for OPC package extracted into directory. + + `path` is the path to a directory containing an expanded package. + """ + + def __init__(self, path: str): + self._path = os.path.abspath(path) + + def __contains__(self, pack_uri: object) -> bool: + """Return True when part identified by `pack_uri` is present in zip archive.""" + if not isinstance(pack_uri, PackURI): + return False + return os.path.exists(posixpath.join(self._path, pack_uri.membername)) + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Return bytes of file corresponding to `pack_uri` in package directory.""" + path = os.path.join(self._path, pack_uri.membername) + try: + with open(path, "rb") as f: + return f.read() + except IOError: + raise KeyError("no member '%s' in package" % pack_uri) + + +class _ZipPkgReader(_PhysPkgReader): + """Implements |PhysPkgReader| interface for a zip-file OPC package.""" + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + def __contains__(self, pack_uri: object) -> bool: + """Return True when part identified by `pack_uri` is present in zip archive.""" + return pack_uri in self._blobs + + def __getitem__(self, pack_uri: PackURI) -> bytes: + """Return bytes for part corresponding to `pack_uri`. + + Raises |KeyError| if no matching member is present in zip archive. + """ + if pack_uri not in self._blobs: + raise KeyError("no member '%s' in package" % pack_uri) + return self._blobs[pack_uri] + + @lazyproperty + def _blobs(self) -> dict[PackURI, bytes]: + """dict mapping partname to package part binaries.""" + with zipfile.ZipFile(self._pkg_file, "r") as z: + return {PackURI("/%s" % name): z.read(name) for name in z.namelist()} + + +class _PhysPkgWriter: + """Base class for physical package writer objects.""" + + @classmethod + def factory(cls, pkg_file: str | IO[bytes]) -> _ZipPkgWriter: + """Return |_PhysPkgWriter| subtype instance appropriage for `pkg_file`. + + Currently the only subtype is `_ZipPkgWriter`, but a `_DirPkgWriter` could be implemented + or even a `_StreamPkgWriter`. + """ + return _ZipPkgWriter(pkg_file) + + def write(self, pack_uri: PackURI, blob: bytes) -> None: + """Write `blob` to package with membername corresponding to `pack_uri`.""" + raise NotImplementedError( # pragma: no cover + f"`{type(self).__name__}` must implement `.write()`" + ) + + +class _ZipPkgWriter(_PhysPkgWriter): + """Implements |PhysPkgWriter| interface for a zip-file (.pptx file) OPC package.""" + + def __init__(self, pkg_file: str | IO[bytes]): + self._pkg_file = pkg_file + + def __enter__(self) -> _ZipPkgWriter: + """Enable use as a context-manager. Opening zip for writing happens here.""" + return self + + def __exit__(self, *exc: list[Any]) -> None: + """Close the zip archive on exit from context. + + Closing flushes any pending physical writes and releasing any resources it's using. + """ + self._zipf.close() + + def write(self, pack_uri: PackURI, blob: bytes) -> None: + """Write `blob` to zip package with membername corresponding to `pack_uri`.""" + self._zipf.writestr(pack_uri.membername, blob) + + @lazyproperty + def _zipf(self) -> zipfile.ZipFile: + """`ZipFile` instance open for writing.""" + return zipfile.ZipFile( + self._pkg_file, "w", compression=zipfile.ZIP_DEFLATED, strict_timestamps=False + ) + + +class _ContentTypesItem: + """Composes content-types "part" ([Content_Types].xml) for a collection of parts.""" + + def __init__(self, parts: Sequence[Part]): + self._parts = parts + + @classmethod + def xml_for(cls, parts: Sequence[Part]) -> CT_Types: + """Return content-types XML mapping each part in `parts` to a content-type. + + The resulting XML is suitable for storage as `[Content_Types].xml` in an OPC package. + """ + return cls(parts)._xml + + @lazyproperty + def _xml(self) -> CT_Types: + """lxml.etree._Element containing the content-types item. + + This XML object is suitable for serialization to the `[Content_Types].xml` item for an OPC + package. Although the sequence of elements is not strictly significant, as an aid to + testing and readability Default elements are sorted by extension and Override elements are + sorted by partname. + """ + defaults, overrides = self._defaults_and_overrides + _types_elm = CT_Types.new() + + for ext, content_type in sorted(defaults.items()): + _types_elm.add_default(ext, content_type) + for partname, content_type in sorted(overrides.items()): + _types_elm.add_override(partname, content_type) + + return _types_elm + + @lazyproperty + def _defaults_and_overrides(self) -> tuple[dict[str, str], dict[PackURI, str]]: + """pair of dict (defaults, overrides) accounting for all parts. + + `defaults` is {ext: content_type} and overrides is {partname: content_type}. + """ + defaults = CaseInsensitiveDict(rels=CT.OPC_RELATIONSHIPS, xml=CT.XML) + overrides: dict[PackURI, str] = {} + + for part in self._parts: + partname, content_type = part.partname, part.content_type + ext = partname.ext + if (ext.lower(), content_type) in default_content_types: + defaults[ext] = content_type + else: + overrides[partname] = content_type + + return defaults, overrides diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/shared.py b/.venv/lib/python3.12/site-packages/pptx/opc/shared.py new file mode 100644 index 00000000..cc7fce8c --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/shared.py @@ -0,0 +1,20 @@ +"""Objects shared by modules in the pptx.opc sub-package.""" + +from __future__ import annotations + + +class CaseInsensitiveDict(dict): + """Mapping type like dict except it matches key without respect to case. + + For example, D['A'] == D['a']. Note this is not general-purpose, just complete + enough to satisfy opc package needs. It assumes str keys for example. + """ + + def __contains__(self, key): + return super(CaseInsensitiveDict, self).__contains__(key.lower()) + + def __getitem__(self, key): + return super(CaseInsensitiveDict, self).__getitem__(key.lower()) + + def __setitem__(self, key, value): + return super(CaseInsensitiveDict, self).__setitem__(key.lower(), value) diff --git a/.venv/lib/python3.12/site-packages/pptx/opc/spec.py b/.venv/lib/python3.12/site-packages/pptx/opc/spec.py new file mode 100644 index 00000000..a83caf8b --- /dev/null +++ b/.venv/lib/python3.12/site-packages/pptx/opc/spec.py @@ -0,0 +1,44 @@ +"""Provides mappings that embody aspects of the Open XML spec ISO/IEC 29500.""" + +from pptx.opc.constants import CONTENT_TYPE as CT + +default_content_types = ( + ("bin", CT.PML_PRINTER_SETTINGS), + ("bin", CT.SML_PRINTER_SETTINGS), + ("bin", CT.WML_PRINTER_SETTINGS), + ("bmp", CT.BMP), + ("emf", CT.X_EMF), + ("fntdata", CT.X_FONTDATA), + ("gif", CT.GIF), + ("jpe", CT.JPEG), + ("jpeg", CT.JPEG), + ("jpg", CT.JPEG), + ("mov", CT.MOV), + ("mp4", CT.MP4), + ("mpg", CT.MPG), + ("png", CT.PNG), + ("rels", CT.OPC_RELATIONSHIPS), + ("tif", CT.TIFF), + ("tiff", CT.TIFF), + ("vid", CT.VIDEO), + ("wdp", CT.MS_PHOTO), + ("wmf", CT.X_WMF), + ("wmv", CT.WMV), + ("xlsx", CT.SML_SHEET), + ("xml", CT.XML), +) + + +image_content_types = { + "bmp": CT.BMP, + "emf": CT.X_EMF, + "gif": CT.GIF, + "jpe": CT.JPEG, + "jpeg": CT.JPEG, + "jpg": CT.JPEG, + "png": CT.PNG, + "tif": CT.TIFF, + "tiff": CT.TIFF, + "wdp": CT.MS_PHOTO, + "wmf": CT.X_WMF, +} |
