init: 폴더구조 설계 및 인프라 설계
This commit is contained in:
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -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"
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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")
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user