diff --git a/.github/workflows/pythonpublish.yml b/.github/workflows/pythonpublish.yml new file mode 100644 index 000000000..521fffae6 --- /dev/null +++ b/.github/workflows/pythonpublish.yml @@ -0,0 +1,31 @@ +# This workflows will upload a Python Package using Twine when a release is created +# For more information see: https://help.github.com/en/actions/language-and-framework-guides/using-python-with-github-actions#publishing-to-package-registries + +name: Publish on PYPI + +on: + release: + types: [created] + +jobs: + deploy: + + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - name: Set up Python + uses: actions/setup-python@v2 + with: + python-version: '3.x' + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install setuptools wheel twine + - name: Build and publish + env: + TWINE_USERNAME: ${{ secrets.PYPI_USERNAME }} + TWINE_PASSWORD: ${{ secrets.PYPI_PASSWORD }} + run: | + python setup.py sdist bdist_wheel + twine upload dist/* diff --git a/.gitignore b/.gitignore index e24445137..9ce46ccb8 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,8 @@ _scratch/ Session.vim /.tox/ +/build/ +/tests/ +/features/ +/docs/ +/ref/ \ No newline at end of file diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 000000000..8e79b0ab8 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,14 @@ +## Bayoo-docx contributors + +============================================ + +* **[Obay Daba](https://github.com/bayoog)** + +* **[Bassel Al Madani](https://github.com/pepos9)** + +* **[Tareq Ibrahim](https://github.com/idtareq)** + +* **[baltazarix](https://github.com/baltazarix)** + +* **[Ahmad Alwareh](https://github.com/ahmadalwareh)** + diff --git a/DESCRIPTION.rst b/DESCRIPTION.rst new file mode 100644 index 000000000..f52196ec2 --- /dev/null +++ b/DESCRIPTION.rst @@ -0,0 +1,42 @@ +Bayoo-docx + + +Python library forked from `python-docx `_. + +The main purpose of the fork was to add implementation for comments and footnotes to the library + +Installation + + +Use the package manager `pip `_ to install bayoo-docx. + + +`pip install bayoo-docx` + +Usage + + +:: + + import docx + + document = docx.Document() + + paragraph1 = document.add_paragraph('text') # create new paragraph + + comment = paragraph.add_comment('comment',author='Obay Daba',initials= 'od') # add a comment on the entire paragraph + + paragraph2 = document.add_paragraph('text') # create another paragraph + + run = paragraph2.add_run('texty') add a run to the paragraph + + run.add_comment('comment') # add a comment only for the run text + + paragraph.add_footnote('footnote text') # add a footnote + + + +License + + +`MIT `_ diff --git a/HISTORY.rst b/HISTORY.rst index 5612cbf05..d94b29456 100644 --- a/HISTORY.rst +++ b/HISTORY.rst @@ -1,264 +1,24 @@ .. :changelog: -Release History ---------------- -0.8.10 (2019-01-08) -+++++++++++++++++++ +#Release History BayooG/bayoo-docx forked from (python-openxmm/python-docx) -- Revert use of expanded package directory for default.docx to work around setup.py - problem with filenames containing square brackets. +0.2.8 (2020-05-02) -0.8.9 (2019-01-08) -++++++++++++++++++ +- add comments implementation on a run level +- fix issue with comments date (comments dates are set to current date) -- Fix gap in MANIFEST.in that excluded default document template directory +0.2.4 (2019-9-4) -0.8.8 (2019-01-07) -++++++++++++++++++ +- loop over all the document chieldern (Paragraphs, Tables, Sections) with the right order `document.elements` +- addons to Paragraph Object (delete, heading_level, merge_paragraph ) +- Add low-level implementation for comments part +- Add oxml element for element and sub-elements +- Add add_comment() method for docx.text.Paragraph +- Add low-level implementation for footnotes part +- Add oxml element for element and sub-elements +- Add add_footnote() method for docx.text.Paragraph -- Add support for headers and footers - -0.8.7 (2018-08-18) -++++++++++++++++++ - -- Add _Row.height_rule -- Add _Row.height -- Add _Cell.vertical_alignment -- Fix #455: increment next_id, don't fill gaps -- Add #375: import docx failure on --OO optimization -- Add #254: remove default zoom percentage -- Add #266: miscellaneous documentation fixes -- Add #175: refine MANIFEST.ini -- Add #168: Unicode error on core-props in Python 2 - - -0.8.6 (2016-06-22) -++++++++++++++++++ - -- Add #257: add Font.highlight_color -- Add #261: add ParagraphFormat.tab_stops -- Add #303: disallow XML entity expansion - - -0.8.5 (2015-02-21) -++++++++++++++++++ - -- Fix #149: KeyError on Document.add_table() -- Fix #78: feature: add_table() sets cell widths -- Add #106: feature: Table.direction (i.e. right-to-left) -- Add #102: feature: add CT_Row.trPr - - -0.8.4 (2015-02-20) -++++++++++++++++++ - -- Fix #151: tests won't run on PyPI distribution -- Fix #124: default to inches on no TIFF resolution unit - - -0.8.3 (2015-02-19) -++++++++++++++++++ - -- Add #121, #135, #139: feature: Font.color - - -0.8.2 (2015-02-16) -++++++++++++++++++ - -- Fix #94: picture prints at wrong size when scaled -- Extract `docx.document.Document` object from `DocumentPart` - - Refactor `docx.Document` from an object into a factory function for new - `docx.document.Document object`. Extract methods from prior `docx.Document` - and `docx.parts.document.DocumentPart` to form the new API class and retire - `docx.Document` class. - -- Migrate `Document.numbering_part` to `DocumentPart.numbering_part`. The - `numbering_part` property is not part of the published API and is an - interim internal feature to be replaced in a future release, perhaps with - something like `Document.numbering_definitions`. In the meantime, it can - now be accessed using ``Document.part.numbering_part``. - - -0.8.1 (2015-02-10) -++++++++++++++++++ - -- Fix #140: Warning triggered on Document.add_heading/table() - - -0.8.0 (2015-02-08) -++++++++++++++++++ - -- Add styles. Provides general capability to access and manipulate paragraph, - character, and table styles. - -- Add ParagraphFormat object, accessible on Paragraph.paragraph_format, and - providing the following paragraph formatting properties: - - + paragraph alignment (justfification) - + space before and after paragraph - + line spacing - + indentation - + keep together, keep with next, page break before, and widow control - -- Add Font object, accessible on Run.font, providing character-level - formatting including: - - + typeface (e.g. 'Arial') - + point size - + underline - + italic - + bold - + superscript and subscript - -The following issues were retired: - -- Add feature #56: superscript/subscript -- Add feature #67: lookup style by UI name -- Add feature #98: Paragraph indentation -- Add feature #120: Document.styles - -**Backward incompatibilities** - -Paragraph.style now returns a Style object. Previously it returned the style -name as a string. The name can now be retrieved using the Style.name -property, for example, `paragraph.style.name`. - - -0.7.6 (2014-12-14) -++++++++++++++++++ - -- Add feature #69: Table.alignment -- Add feature #29: Document.core_properties - - -0.7.5 (2014-11-29) -++++++++++++++++++ - -- Add feature #65: _Cell.merge() - - -0.7.4 (2014-07-18) -++++++++++++++++++ - -- Add feature #45: _Cell.add_table() -- Add feature #76: _Cell.add_paragraph() -- Add _Cell.tables property (read-only) - - -0.7.3 (2014-07-14) -++++++++++++++++++ - -- Add Table.autofit -- Add feature #46: _Cell.width - - -0.7.2 (2014-07-13) -++++++++++++++++++ - -- Fix: Word does not interpret as line feed - - -0.7.1 (2014-07-11) -++++++++++++++++++ - -- Add feature #14: Run.add_picture() - - -0.7.0 (2014-06-27) -++++++++++++++++++ - -- Add feature #68: Paragraph.insert_paragraph_before() -- Add feature #51: Paragraph.alignment (read/write) -- Add feature #61: Paragraph.text setter -- Add feature #58: Run.add_tab() -- Add feature #70: Run.clear() -- Add feature #60: Run.text setter -- Add feature #39: Run.text and Paragraph.text interpret '\n' and '\t' chars - - -0.6.0 (2014-06-22) -++++++++++++++++++ - -- Add feature #15: section page size -- Add feature #66: add section -- Add page margins and page orientation properties on Section -- Major refactoring of oxml layer - - -0.5.3 (2014-05-10) -++++++++++++++++++ - -- Add feature #19: Run.underline property - - -0.5.2 (2014-05-06) -++++++++++++++++++ - -- Add feature #17: character style - - -0.5.1 (2014-04-02) -++++++++++++++++++ - -- Fix issue #23, `Document.add_picture()` raises ValueError when document - contains VML drawing. - - -0.5.0 (2014-03-02) -++++++++++++++++++ - -- Add 20 tri-state properties on Run, including all-caps, double-strike, - hidden, shadow, small-caps, and 15 others. - - -0.4.0 (2014-03-01) -++++++++++++++++++ - -- Advance from alpha to beta status. -- Add pure-python image header parsing; drop Pillow dependency - - -0.3.0a5 (2014-01-10) -++++++++++++++++++++++ - -- Hotfix: issue #4, Document.add_picture() fails on second and subsequent - images. - - -0.3.0a4 (2014-01-07) -++++++++++++++++++++++ - -- Complete Python 3 support, tested on Python 3.3 - - -0.3.0a3 (2014-01-06) -++++++++++++++++++++++ - -- Fix setup.py error on some Windows installs - - -0.3.0a1 (2014-01-05) -++++++++++++++++++++++ - -- Full object-oriented rewrite -- Feature-parity with prior version -- text: add paragraph, run, text, bold, italic -- table: add table, add row, add column -- styles: specify style for paragraph, table -- picture: add inline picture, auto-scaling -- breaks: add page break -- tests: full pytest and behave-based 2-layer test suite - - -0.3.0dev1 (2013-12-14) -++++++++++++++++++++++ - -- Round-trip .docx file, preserving all parts and relationships -- Load default "template" .docx on open with no filename -- Open from stream and save to stream (file-like object) -- Add paragraph at and of document diff --git a/LICENSE b/LICENSE index 67ebe716e..dece2863c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,6 @@ The MIT License (MIT) -Copyright (c) 2013 Steve Canny, https://github.com/scanny +Copyright (c) 2019 Obay Daba, https://github.com/bayoog +forked from https://github.com/python-openxml/python-docx Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md new file mode 100644 index 000000000..74fb0842e --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +Bayoo-docx +========== + +Python library forked from [python-docx](https://github.com/python-openxml/python-docx). + +The main purpose of the fork was to add implementation for comments and footnotes to the library + +Installation +------------ + +Use the package manager [pip](https://pypi.org/project/bayoo-docx/) to install bayoo-docx. + + +`pip install bayoo-docx` + +Usage: +----- + + + + import docx + + document = docx.Document() + + paragraph = document.add_paragraph('text') # create new paragraph + + comment = paragraph.add_comment('comment',author='Obay Daba',initials= 'od') # add a comment on the entire paragraph + + paragraph2 = document.add_paragraph('text') # create another paragraph + + run = paragraph2.add_run('text1') #add a run to the paragraph + + run.add_comment('comment') # add a comment only for the run text + + run.add_comment('comment2') + + run_comments = run.comments + + paragraph.add_footnote('footnote text') # add a footnote + + diff --git a/README.rst b/README.rst deleted file mode 100644 index 82d1f0bd7..000000000 --- a/README.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. image:: https://travis-ci.org/python-openxml/python-docx.svg?branch=master - :target: https://travis-ci.org/python-openxml/python-docx - -*python-docx* is a Python library for creating and updating Microsoft Word -(.docx) files. - -More information is available in the `python-docx documentation`_. - -.. _`python-docx documentation`: - https://python-docx.readthedocs.org/en/latest/ diff --git a/docx/__init__.py b/docx/__init__.py index 4dae2946b..ee8a52ad1 100644 --- a/docx/__init__.py +++ b/docx/__init__.py @@ -2,7 +2,7 @@ from docx.api import Document # noqa -__version__ = '0.8.10' +__version__ = '0.2.20' # register custom Part classes with opc package reader @@ -17,6 +17,8 @@ from docx.parts.numbering import NumberingPart from docx.parts.settings import SettingsPart from docx.parts.styles import StylesPart +from docx.parts.comments import CommentsPart +from docx.parts.footnotes import FootnotesPart def part_class_selector(content_type, reltype): @@ -26,6 +28,7 @@ def part_class_selector(content_type, reltype): PartFactory.part_class_selector = part_class_selector +PartFactory.part_type_for[CT.WML_COMMENTS] = CommentsPart PartFactory.part_type_for[CT.OPC_CORE_PROPERTIES] = CorePropertiesPart PartFactory.part_type_for[CT.WML_DOCUMENT_MAIN] = DocumentPart PartFactory.part_type_for[CT.WML_FOOTER] = FooterPart @@ -33,6 +36,7 @@ def part_class_selector(content_type, reltype): PartFactory.part_type_for[CT.WML_NUMBERING] = NumberingPart PartFactory.part_type_for[CT.WML_SETTINGS] = SettingsPart PartFactory.part_type_for[CT.WML_STYLES] = StylesPart +PartFactory.part_type_for[CT.WML_FOOTNOTES] = FootnotesPart del ( CT, @@ -40,6 +44,8 @@ def part_class_selector(content_type, reltype): DocumentPart, FooterPart, HeaderPart, + FootnotesPart, + CommentsPart, NumberingPart, PartFactory, SettingsPart, diff --git a/docx/api.py b/docx/api.py index 63e18c406..1cc5fad34 100644 --- a/docx/api.py +++ b/docx/api.py @@ -35,3 +35,15 @@ def _default_docx_path(): """ _thisdir = os.path.split(__file__)[0] return os.path.join(_thisdir, 'templates', 'default.docx') + + +def element(element, part): + if str(type(element)) == "": + from .text.paragraph import Paragraph + return Paragraph(element, part) + elif str(type(element)) == "": + from .table import Table + return Table(element, part) + elif str(type(element)) == "": + from .section import Section + return Section(element, part) \ No newline at end of file diff --git a/docx/blkcntnr.py b/docx/blkcntnr.py index a80903e52..5c9810060 100644 --- a/docx/blkcntnr.py +++ b/docx/blkcntnr.py @@ -9,9 +9,10 @@ from __future__ import absolute_import, division, print_function, unicode_literals from docx.oxml.table import CT_Tbl +from docx.oxml.ns import qn from docx.shared import Parented from docx.text.paragraph import Paragraph - +from docx.api import element class BlockItemContainer(Parented): """Base class for proxy objects that can contain block items. @@ -66,10 +67,23 @@ def tables(self): """ from .table import Table return [Table(tbl, self) for tbl in self._element.tbl_lst] + @property + def elements(self): + """ + A list containing the elements in this container (paragraph and tables), in document order. + """ + return [element(item,self.part) for item in self._element.getchildren()] + + + @property + def abstractNumIds(self): + return [numId for numId in self.part.numbering_part.element.iterchildren(qn('w:abstractNum'))] + def _add_paragraph(self): """ Return a paragraph newly added to the end of the content in this container. """ return Paragraph(self._element.add_p(), self) + diff --git a/docx/document.py b/docx/document.py index 6493c458b..a35c85b72 100644 --- a/docx/document.py +++ b/docx/document.py @@ -4,6 +4,8 @@ from __future__ import absolute_import, division, print_function, unicode_literals +from docx.oxml.ns import qn + from docx.blkcntnr import BlockItemContainer from docx.enum.section import WD_SECTION from docx.enum.text import WD_BREAK @@ -101,6 +103,23 @@ def core_properties(self): """ return self._part.core_properties + @property + def comments_part(self): + """ + A |Comments| object providing read/write access to the core + properties of this document. + """ + return self.part.comments_part + + # @property + # def footnotes_part(self): + # """ + # A |Footnotes| object providing read/write access to the core + # properties of this document. + # """ + # return self.part._footnotes_part + + @property def inline_shapes(self): """ @@ -165,6 +184,23 @@ def tables(self): """ return self._body.tables + @property + def elements(self): + return self._body.elements + + @property + def abstractNumIds(self): + """ + Returns list of all the 'w:abstarctNumId' of this document + """ + return self._body.abstractNumIds + + @property + def last_abs_num(self): + last = self.abstractNumIds[-1] + val = last.attrib.get(qn('w:abstractNumId')) + return last, val + @property def _block_width(self): """ diff --git a/docx/image/__init__.py b/docx/image/__init__.py index 8ab3ada68..30b8d45d5 100644 --- a/docx/image/__init__.py +++ b/docx/image/__init__.py @@ -14,7 +14,7 @@ from docx.image.jpeg import Exif, Jfif from docx.image.png import Png from docx.image.tiff import Tiff - +from docx.image.emf import Emf SIGNATURES = ( # class, offset, signature_bytes @@ -26,4 +26,5 @@ (Tiff, 0, b'MM\x00*'), # big-endian (Motorola) TIFF (Tiff, 0, b'II*\x00'), # little-endian (Intel) TIFF (Bmp, 0, b'BM'), + (Emf, 40, b' EMF') ) diff --git a/docx/image/constants.py b/docx/image/constants.py index 90b469705..97d40e314 100644 --- a/docx/image/constants.py +++ b/docx/image/constants.py @@ -102,7 +102,7 @@ class MIME_TYPE(object): JPEG = 'image/jpeg' PNG = 'image/png' TIFF = 'image/tiff' - + EMF = 'image/emf' class PNG_CHUNK_TYPE(object): """ diff --git a/docx/image/emf.py b/docx/image/emf.py new file mode 100644 index 000000000..e2701c04f --- /dev/null +++ b/docx/image/emf.py @@ -0,0 +1,70 @@ +# encoding: utf-8 + +from __future__ import absolute_import, division, print_function + +from .constants import MIME_TYPE +from .exceptions import InvalidImageStreamError +from .helpers import BIG_ENDIAN, StreamReader +from .image import BaseImageHeader +import struct + +class Emf(BaseImageHeader): + """ + Image header parser for PNG images + """ + @property + def content_type(self): + """ + MIME content type for this image, unconditionally `image/png` for + PNG images. + """ + return MIME_TYPE.EMF + + @property + def default_ext(self): + """ + Default filename extension, always 'png' for PNG images. + """ + return 'emf' + + @classmethod + def from_stream(cls, stream,filename=None): + """ + Return a |Emf| instance having header properties parsed from image in + *stream*. + """ + + """ + @0 DWORD iType; // fixed + @4 DWORD nSize; // var + @8 RECTL rclBounds; + @24 RECTL rclFrame; // .01 millimeter units L T R B + @40 DWORD dSignature; // ENHMETA_SIGNATURE = 0x464D4520 + DWORD nVersion; + DWORD nBytes; + DWORD nRecords; + WORD nHandles; + WORD sReserved; + DWORD nDescription; + DWORD offDescription; + DWORD nPalEntries; + SIZEL szlDevice; + SIZEL szlMillimeters; + """ + stream.seek(0) + x = stream.read(40) + stream.seek(0) + iType,nSize = struct.unpack("ii",x[0:8]) + rclBounds = struct.unpack("iiii",x[8:24]) + rclFrame = struct.unpack("iiii",x[24:40]) + + dpi = 300 + horz_dpi = dpi + vert_dpi = dpi + mmwidth = (rclFrame[2]-rclFrame[0])/100.0 + mmheight = (rclFrame[3]-rclFrame[1])/100.0 + px_width = int(mmwidth*dpi*0.03937008) + px_height = int(mmheight*dpi*0.03937008) + + #1 dot/inch = 0.03937008 pixel/millimeter + return cls(px_width,px_height,horz_dpi,vert_dpi) \ No newline at end of file diff --git a/docx/image/image.py b/docx/image/image.py index ba2158e72..3df31672f 100644 --- a/docx/image/image.py +++ b/docx/image/image.py @@ -188,7 +188,7 @@ def _ImageHeaderFactory(stream): def read_32(stream): stream.seek(0) - return stream.read(32) + return stream.read(64) header = read_32(stream) for cls, offset, signature_bytes in SIGNATURES: diff --git a/docx/opc/package.py b/docx/opc/package.py index 7ba87bab5..653151642 100644 --- a/docx/opc/package.py +++ b/docx/opc/package.py @@ -10,6 +10,8 @@ from docx.opc.parts.coreprops import CorePropertiesPart from docx.opc.pkgreader import PackageReader from docx.opc.pkgwriter import PackageWriter +from docx.parts.comments import CommentsPart +from docx.parts.footnotes import FootnotesPart from docx.opc.rel import Relationships from docx.opc.shared import lazyproperty @@ -183,6 +185,32 @@ def _core_properties_part(self): core_properties_part = CorePropertiesPart.default(self) self.relate_to(core_properties_part, RT.CORE_PROPERTIES) return core_properties_part + + @property + def _comments_part(self): + """ + |CommentsPart| object related to this package. Creates + a default Comments part if one is not present. + """ + try: + return self.part_related_by(RT.COMMENTS) + except KeyError: + comments_part = CommentsPart.default(self) + self.relate_to(comments_part, RT.COMMENTS) + return comments_part + + @property + def _footnotes_part(self): + """ + |FootnotesPart| object related to this package. Creates + a default Comments part if one is not present. + """ + try: + return self.part_related_by(RT.FOOTNOTES) + except KeyError: + footnotes_part = FootnotesPart.default(self) + self.relate_to(footnotes_part, RT.FOOTNOTES) + return footnotes_part class Unmarshaller(object): diff --git a/docx/oxml/__init__.py b/docx/oxml/__init__.py index 093c1b45b..21f610edf 100644 --- a/docx/oxml/__init__.py +++ b/docx/oxml/__init__.py @@ -76,11 +76,12 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:body', CT_Body) register_element_cls('w:document', CT_Document) -from .numbering import CT_Num, CT_Numbering, CT_NumLvl, CT_NumPr # noqa +from .numbering import CT_Num, CT_AbstractNum, CT_Numbering, CT_NumLvl, CT_NumPr # noqa register_element_cls('w:abstractNumId', CT_DecimalNumber) register_element_cls('w:ilvl', CT_DecimalNumber) register_element_cls('w:lvlOverride', CT_NumLvl) register_element_cls('w:num', CT_Num) +register_element_cls('w:abstractNum', CT_AbstractNum) register_element_cls('w:numId', CT_DecimalNumber) register_element_cls('w:numPr', CT_NumPr) register_element_cls('w:numbering', CT_Numbering) @@ -135,8 +136,11 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('wp:extent', CT_PositiveSize2D) register_element_cls('wp:inline', CT_Inline) -from .styles import CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles # noqa +from .styles import CT_DocDefaults, CT_RPrDefault, CT_PPrDefault, CT_LatentStyles, CT_LsdException, CT_Style, CT_Styles # noqa register_element_cls('w:basedOn', CT_String) +register_element_cls('w:docDefaults', CT_DocDefaults) +register_element_cls('w:rPrDefault', CT_RPrDefault) +register_element_cls('w:pPrDefault', CT_PPrDefault) register_element_cls('w:latentStyles', CT_LatentStyles) register_element_cls('w:locked', CT_OnOff) register_element_cls('w:lsdException', CT_LsdException) @@ -162,7 +166,11 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): CT_TcPr, CT_TrPr, CT_VMerge, + CT_TblMar, CT_VerticalJc, + CT_TblBoarders, + CT_Bottom, + CT_TcBorders, ) register_element_cls('w:bidiVisual', CT_OnOff) register_element_cls('w:gridCol', CT_TblGridCol) @@ -171,6 +179,8 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:tblGrid', CT_TblGrid) register_element_cls('w:tblLayout', CT_TblLayoutType) register_element_cls('w:tblPr', CT_TblPr) +register_element_cls('w:tblW', CT_TblWidth) +register_element_cls('w:tblCellMar', CT_TblMar) register_element_cls('w:tblStyle', CT_String) register_element_cls('w:tc', CT_Tc) register_element_cls('w:tcPr', CT_TcPr) @@ -180,6 +190,9 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:trPr', CT_TrPr) register_element_cls('w:vAlign', CT_VerticalJc) register_element_cls('w:vMerge', CT_VMerge) +register_element_cls('w:tblBorders', CT_TblBoarders) +register_element_cls('w:tcBorders', CT_TcBorders) +register_element_cls('w:bottom', CT_Bottom) from .text.font import ( # noqa CT_Color, @@ -246,3 +259,20 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None): register_element_cls('w:br', CT_Br) register_element_cls('w:r', CT_R) register_element_cls('w:t', CT_Text) +register_element_cls('w:rPr', CT_RPr) + + +from .comments import CT_Comments,CT_Com, CT_CRE, CT_CRS, CT_CRef +register_element_cls('w:comments', CT_Comments) +register_element_cls('w:comment', CT_Com) +register_element_cls('w:commentRangeStart', CT_CRS) +register_element_cls('w:commentRangeEnd', CT_CRE) +register_element_cls('w:commentReference', CT_CRef) + + +from .footnotes import CT_Footnotes, CT_Footnote, CT_FNR, CT_FootnoteRef + +register_element_cls('w:footnotes', CT_Footnotes) +register_element_cls('w:footnote', CT_Footnote) +register_element_cls('w:footnoteReference', CT_FNR) +register_element_cls('w:footnoteRef', CT_FootnoteRef) \ No newline at end of file diff --git a/docx/oxml/comments.py b/docx/oxml/comments.py new file mode 100644 index 000000000..214a88f20 --- /dev/null +++ b/docx/oxml/comments.py @@ -0,0 +1,127 @@ +""" +Custom element classes related to the comments part +""" + +from . import OxmlElement +from .simpletypes import ST_DecimalNumber, ST_String +from ..opc.constants import NAMESPACE +from ..text.paragraph import Paragraph +from ..text.run import Run +from .xmlchemy import ( + BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrMore, ZeroOrOne +) + +class CT_Com(BaseOxmlElement): + """ + A ```` element, a container for Comment properties + """ + initials = RequiredAttribute('w:initials', ST_String) + _id = RequiredAttribute('w:id', ST_DecimalNumber) + date = RequiredAttribute('w:date', ST_String) + author = RequiredAttribute('w:author', ST_String) + + p = ZeroOrOne('w:p', successors=('w:comment',)) + + @classmethod + def new(cls, initials, comm_id, date, author): + """ + Return a new ```` element having _id of *comm_id* and having + the passed params as meta data + """ + comment = OxmlElement('w:comment') + comment.initials = initials + comment.date = date + comment._id = comm_id + comment.author = author + return comment + + def _add_p(self, text): + _p = OxmlElement('w:p') + _r = _p.add_r() + run = Run(_r,self) + run.text = text + self._insert_p(_p) + return _p + + @property + def meta(self): + return [self.author, self.initials, self.date] + + @property + def paragraph(self): + return Paragraph(self.p, self) + + +class CT_Comments(BaseOxmlElement): + """ + A ```` element, a container for Comments properties + """ + comment = ZeroOrMore ('w:comment', successors=('w:comments',)) + + def add_comment(self,author, initials, date): + _next_id = self._next_commentId + comment = CT_Com.new(initials, _next_id, date, author) + comment = self._insert_comment(comment) + + return comment + + @property + def _next_commentId(self): + ids = self.xpath('./w:comment/@w:id') + len(ids) + _ids = [int(_str) for _str in ids] + _ids.sort() + + try: + return _ids[-1] + 2 + except: + return 0 + + def get_comment_by_id(self, _id): + namesapce = NAMESPACE().WML_MAIN + for c in self.findall('.//w:comment',{'w':namesapce}): + if c._id == _id: + return c + return None + + +class CT_CRS(BaseOxmlElement): + """ + A ```` element + """ + _id = RequiredAttribute('w:id', ST_DecimalNumber) + + @classmethod + def new(cls, _id): + commentRangeStart = OxmlElement('w:commentRangeStart') + commentRangeStart._id =_id + + return commentRangeStart + +class CT_CRE(BaseOxmlElement): + """ + A ``w:commentRangeEnd`` element + """ + _id = RequiredAttribute('w:id', ST_DecimalNumber) + + + @classmethod + def new(cls, _id): + commentRangeEnd = OxmlElement('w:commentRangeEnd') + commentRangeEnd._id =_id + return commentRangeEnd + + +class CT_CRef(BaseOxmlElement): + """ + w:commentReference + """ + _id = RequiredAttribute('w:id', ST_DecimalNumber) + + @classmethod + def new (cls, _id): + commentReference = OxmlElement('w:commentReference') + commentReference._id =_id + return commentReference + + diff --git a/docx/oxml/footnotes.py b/docx/oxml/footnotes.py new file mode 100644 index 000000000..90c791bc3 --- /dev/null +++ b/docx/oxml/footnotes.py @@ -0,0 +1,89 @@ +""" +Custom element classes related to the footnotes part +""" + + +from . import OxmlElement +from .simpletypes import ST_DecimalNumber, ST_String +from ..text.paragraph import Paragraph +from ..text.run import Run +from ..opc.constants import NAMESPACE +from .xmlchemy import ( + BaseOxmlElement, OneAndOnlyOne, RequiredAttribute, ZeroOrMore, ZeroOrOne +) + + +class CT_Footnotes(BaseOxmlElement): + """ + A ```` element, a container for Footnotes properties + """ + + footnote = ZeroOrMore ('w:footnote', successors=('w:footnotes',)) + + @property + def _next_id(self): + ids = self.xpath('./w:footnote/@w:id') + + return int(ids[-1]) + 1 + + def add_footnote(self): + _next_id = self._next_id + footnote = CT_Footnote.new(_next_id) + footnote = self._insert_footnote(footnote) + return footnote + + def get_footnote_by_id(self, _id): + namesapce = NAMESPACE().WML_MAIN + for fn in self.findall('.//w:footnote', {'w':namesapce}): + if fn._id == _id: + return fn + return None + +class CT_Footnote(BaseOxmlElement): + """ + A ```` element, a container for Footnote properties + """ + _id = RequiredAttribute('w:id', ST_DecimalNumber) + p = ZeroOrOne('w:p', successors=('w:footnote',)) + + @classmethod + def new(cls, _id): + footnote = OxmlElement('w:footnote') + footnote._id = _id + + return footnote + + def _add_p(self, text): + _p = OxmlElement('w:p') + _p.footnote_style() + + _r = _p.add_r() + _r.footnote_style() + _r = _p.add_r() + _r.add_footnoteRef() + + run = Run(_r, self) + run.text = text + + self._insert_p(_p) + return _p + + @property + def paragraph(self): + return Paragraph(self.p, self) + +class CT_FNR(BaseOxmlElement): + _id = RequiredAttribute('w:id', ST_DecimalNumber) + + @classmethod + def new (cls, _id): + footnoteReference = OxmlElement('w:footnoteReference') + footnoteReference._id = _id + return footnoteReference + +class CT_FootnoteRef (BaseOxmlElement): + + @classmethod + def new (cls): + ref = OxmlElement('w:footnoteRef') + return ref \ No newline at end of file diff --git a/docx/oxml/numbering.py b/docx/oxml/numbering.py index aeedfa9a0..6f26ee150 100644 --- a/docx/oxml/numbering.py +++ b/docx/oxml/numbering.py @@ -45,6 +45,13 @@ def new(cls, num_id, abstractNum_id): return num +class CT_AbstractNum(BaseOxmlElement): + """ + ```` element, which represents an abstract numbering definition that defines most of the formatting details. + """ + abstractNumId = RequiredAttribute('w:abstractNumId', ST_DecimalNumber) + + class CT_NumLvl(BaseOxmlElement): """ ```` element, which identifies a level in a list @@ -94,6 +101,7 @@ class CT_Numbering(BaseOxmlElement): ```` element, the root element of a numbering part, i.e. numbering.xml """ + abstractNum = ZeroOrMore('w:abstractNum', successors=('w:num',)) num = ZeroOrMore('w:num', successors=('w:numIdMacAtCleanup',)) def add_num(self, abstractNum_id): diff --git a/docx/oxml/section.py b/docx/oxml/section.py index fc953e74d..fd889aa48 100644 --- a/docx/oxml/section.py +++ b/docx/oxml/section.py @@ -54,7 +54,6 @@ class CT_PageSz(BaseOxmlElement): 'w:orient', WD_ORIENTATION, default=WD_ORIENTATION.PORTRAIT ) - class CT_SectPr(BaseOxmlElement): """`w:sectPr` element, the container element for section properties""" @@ -92,6 +91,7 @@ def add_headerReference(self, type_, rId): headerReference.rId = rId return headerReference + @property def bottom_margin(self): """ diff --git a/docx/oxml/styles.py b/docx/oxml/styles.py index 6f27e45eb..4a7483bae 100644 --- a/docx/oxml/styles.py +++ b/docx/oxml/styles.py @@ -31,6 +31,17 @@ def styleId_from_name(name): }.get(name, name.replace(' ', '')) +class CT_DocDefaults(BaseOxmlElement): + _tag_seq = ('w:rPrDefault', 'w:pPrDefault') + rPrDefault = ZeroOrOne('w:rPrDefault', successors=(_tag_seq[1:])) + pPrDefault = ZeroOrOne('w:pPrDefault', successors=()) + +class CT_RPrDefault(BaseOxmlElement): + rPr = ZeroOrOne('w:rPr', successors=()) + +class CT_PPrDefault(BaseOxmlElement): + pPr = ZeroOrOne('w:pPr', successors=()) + class CT_LatentStyles(BaseOxmlElement): """ `w:latentStyles` element, defining behavior defaults for latent styles @@ -292,6 +303,7 @@ class CT_Styles(BaseOxmlElement): styles.xml """ _tag_seq = ('w:docDefaults', 'w:latentStyles', 'w:style') + docDefaults = ZeroOrOne('w:docDefaults', successors=_tag_seq[1:]) latentStyles = ZeroOrOne('w:latentStyles', successors=_tag_seq[2:]) style = ZeroOrMore('w:style', successors=()) del _tag_seq diff --git a/docx/oxml/table.py b/docx/oxml/table.py index e55bf9126..387b05063 100644 --- a/docx/oxml/table.py +++ b/docx/oxml/table.py @@ -7,12 +7,13 @@ ) from . import parse_xml +from . import OxmlElement from ..enum.table import WD_CELL_VERTICAL_ALIGNMENT, WD_ROW_HEIGHT_RULE from ..exceptions import InvalidSpanError -from .ns import nsdecls, qn +from .ns import nsdecls, qn, nsmap from ..shared import Emu, Twips from .simpletypes import ( - ST_Merge, ST_TblLayoutType, ST_TblWidth, ST_TwipsMeasure, XsdInt + ST_Merge, ST_TblLayoutType, ST_TblWidth, ST_TwipsMeasure, XsdInt, ST_String ) from .xmlchemy import ( BaseOxmlElement, OneAndOnlyOne, OneOrMore, OptionalAttribute, @@ -233,6 +234,20 @@ def _tcs_xml(cls, col_count, col_width): ) % col_width.twips return xml + @property + def _section(self): + body = self.getparent() + sections = body.findall('.//w:sectPr', {'w':nsmap['w']}) + if len(sections) == 1: + return sections[0] + else: + tbl_index = body.index(self) + for i,sect in enumerate(sections): + if i == len(sections) - 1 : + return sect + else: + if body.index(sect.getparent().getparent()) > tbl_index: + return sect class CT_TblGrid(BaseOxmlElement): """ @@ -265,6 +280,8 @@ class CT_TblLayoutType(BaseOxmlElement): """ type = OptionalAttribute('w:type', ST_TblLayoutType) +class CT_TblBoarders(BaseOxmlElement): + pass class CT_TblPr(BaseOxmlElement): """ @@ -280,8 +297,11 @@ class CT_TblPr(BaseOxmlElement): ) tblStyle = ZeroOrOne('w:tblStyle', successors=_tag_seq[1:]) bidiVisual = ZeroOrOne('w:bidiVisual', successors=_tag_seq[4:]) + tblW =ZeroOrOne ('w:tblW', successors=('w:tblPr',)) + tblCellMar = ZeroOrOne('w:tblCellMar', successors=('w:tblPr',)) jc = ZeroOrOne('w:jc', successors=_tag_seq[8:]) tblLayout = ZeroOrOne('w:tblLayout', successors=_tag_seq[13:]) + tblBorders = ZeroOrOne('w:tblBorders', successors=('w:tblPr',)) del _tag_seq @property @@ -747,6 +767,29 @@ def _tr_idx(self): """ return self._tbl.tr_lst.index(self._tr) +class CT_TcBorders(BaseOxmlElement): + """ + element + """ + top = ZeroOrOne('w:top') + start = ZeroOrOne('w:start') + bottom = ZeroOrOne('w:bottom',successors=('w:tblPr',) ) + end = ZeroOrOne('w:end') + + + def new(cls): + """ + Return a new ```` element + """ + return parse_xml( + '\n' + '' % nsdecls('w') + ) + + def add_bottom_border(self, val, sz): + bottom = CT_Bottom.new ( val, sz) + return self._insert_bottom(bottom) + class CT_TcPr(BaseOxmlElement): """ @@ -760,8 +803,10 @@ class CT_TcPr(BaseOxmlElement): ) tcW = ZeroOrOne('w:tcW', successors=_tag_seq[2:]) gridSpan = ZeroOrOne('w:gridSpan', successors=_tag_seq[3:]) + tcBorders = ZeroOrOne('w:tcBorders', successors = ('w:tcPr',)) vMerge = ZeroOrOne('w:vMerge', successors=_tag_seq[5:]) vAlign = ZeroOrOne('w:vAlign', successors=_tag_seq[12:]) + del _tag_seq @property @@ -892,3 +937,31 @@ class CT_VMerge(BaseOxmlElement): ```` element, specifying vertical merging behavior of a cell. """ val = OptionalAttribute('w:val', ST_Merge, default=ST_Merge.CONTINUE) + + +class CT_TblMar(BaseOxmlElement): + """ + ```` element + """ + left = ZeroOrOne('w:left', successors=('w:tblCellMar',)) + right = ZeroOrOne('w:write', successors=('w:tblCellMar',)) + + +class CT_Bottom(BaseOxmlElement): + """ + element + """ + val= OptionalAttribute('w:val', ST_String) + sz= OptionalAttribute('w:sz', ST_String) + space = OptionalAttribute('w:space', ST_String) + color = OptionalAttribute('w:color', ST_String) + + @classmethod + def new(cls, val, sz): + bottom = OxmlElement('w:bottom') + bottom.val = val + bottom.sz = sz + bottom.space = "0" + bottom.color = "auto" + + return bottom diff --git a/docx/oxml/text/font.py b/docx/oxml/text/font.py index 810ec2b30..889eac098 100644 --- a/docx/oxml/text/font.py +++ b/docx/oxml/text/font.py @@ -32,6 +32,8 @@ class CT_Fonts(BaseOxmlElement): """ ascii = OptionalAttribute('w:ascii', ST_String) hAnsi = OptionalAttribute('w:hAnsi', ST_String) + asciiTheme = OptionalAttribute('w:asciiTheme', ST_String) + hAnsiTheme = OptionalAttribute('w:hAnsiTheme', ST_String) class CT_Highlight(BaseOxmlElement): @@ -155,6 +157,44 @@ def rFonts_hAnsi(self, value): rFonts = self.get_or_add_rFonts() rFonts.hAnsi = value + @property + def rFonts_asciiTheme(self): + """ + The value of `w:rFonts/@w:asciiTheme` or |None| if not present. Represents + the assigned typeface Theme. The rFonts element also specifies other + special-case typeface Theme; this method handles the case where just + the common Theme is required. + """ + rFonts = self.rFonts + if rFonts is None: + return None + return rFonts.asciiTheme + + @rFonts_asciiTheme.setter + def rFonts_asciiTheme(self, value): + if value is None: + self._remove_rFonts() + return + rFonts = self.get_or_add_rFonts() + rFonts.asciiTheme = value + + @property + def rFonts_hAnsiTheme(self): + """ + The value of `w:rFonts/@w:hAnsiTheme` or |None| if not present. + """ + rFonts = self.rFonts + if rFonts is None: + return None + return rFonts.hAnsiTheme + + @rFonts_hAnsiTheme.setter + def rFonts_hAnsiTheme(self, value): + if value is None and self.rFonts is None: + return + rFonts = self.get_or_add_rFonts() + rFonts.hAnsiTheme = value + @property def style(self): """ diff --git a/docx/oxml/text/paragraph.py b/docx/oxml/text/paragraph.py index 5e4213776..122b65c5f 100644 --- a/docx/oxml/text/paragraph.py +++ b/docx/oxml/text/paragraph.py @@ -26,6 +26,46 @@ def add_p_before(self): new_p = OxmlElement('w:p') self.addprevious(new_p) return new_p + + def link_comment(self, _id, rangeStart=0, rangeEnd=0): + rStart = OxmlElement('w:commentRangeStart') + rStart._id = _id + rEnd = OxmlElement('w:commentRangeEnd') + rEnd._id = _id + if rangeStart == 0 and rangeEnd == 0: + self.insert(0,rStart) + self.append(rEnd) + else: + self.insert(rangeStart,rStart) + if rangeEnd == len(self.getchildren() ) - 1 : + self.append(rEnd) + else: + self.insert(rangeEnd+1, rEnd) + + def add_comm(self, author, comment_part, initials, dtime, comment_text, rangeStart, rangeEnd): + + comment = comment_part.add_comment(author, initials, dtime) + comment._add_p(comment_text) + _r = self.add_r() + _r.add_comment_reference(comment._id) + self.link_comment(comment._id, rangeStart= rangeStart, rangeEnd=rangeEnd) + + return comment + + def add_fn(self, text, footnotes_part): + footnote = footnotes_part.add_footnote() + footnote._add_p(' '+text) + _r = self.add_r() + _r.add_footnote_reference(footnote._id) + + return footnote + + def footnote_style(self): + pPr = self.get_or_add_pPr() + rstyle = pPr.get_or_add_pStyle() + rstyle.val = 'FootnoteText' + + return self @property def alignment(self): @@ -71,7 +111,24 @@ def style(self): if pPr is None: return None return pPr.style + + @property + def comment_id(self): + _id = self.xpath('./w:commentRangeStart/@w:id') + if len(_id) > 1 or len(_id) == 0: + return None + else: + return int(_id[0]) + + @property + def footnote_ids(self): + _id = self.xpath('./w:r/w:footnoteReference/@w:id') + if len(_id) == 0 : + return None + else: + return _id + @style.setter def style(self, style): pPr = self.get_or_add_pPr() diff --git a/docx/oxml/text/parfmt.py b/docx/oxml/text/parfmt.py index 466b11b1b..69900b0dc 100644 --- a/docx/oxml/text/parfmt.py +++ b/docx/oxml/text/parfmt.py @@ -57,6 +57,7 @@ class CT_PPr(BaseOxmlElement): spacing = ZeroOrOne('w:spacing', successors=_tag_seq[22:]) ind = ZeroOrOne('w:ind', successors=_tag_seq[23:]) jc = ZeroOrOne('w:jc', successors=_tag_seq[27:]) + rPr = ZeroOrOne('w:rPr', successors=_tag_seq[34:]) sectPr = ZeroOrOne('w:sectPr', successors=_tag_seq[35:]) del _tag_seq diff --git a/docx/oxml/text/run.py b/docx/oxml/text/run.py index 8f0a62e82..255c45a35 100644 --- a/docx/oxml/text/run.py +++ b/docx/oxml/text/run.py @@ -5,11 +5,15 @@ """ from ..ns import qn -from ..simpletypes import ST_BrClear, ST_BrType +from ..simpletypes import ST_BrClear, ST_BrType, ST_DecimalNumber, ST_String + +from .. import OxmlElement from ..xmlchemy import ( - BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne + BaseOxmlElement, OptionalAttribute, ZeroOrMore, ZeroOrOne, RequiredAttribute ) +from .. import OxmlElement + class CT_Br(BaseOxmlElement): """ @@ -24,6 +28,8 @@ class CT_R(BaseOxmlElement): ```` element, containing the properties and text for a run. """ rPr = ZeroOrOne('w:rPr') + # wrong + ref = ZeroOrOne('w:commentRangeStart', successors=('w:r',)) t = ZeroOrMore('w:t') br = ZeroOrMore('w:br') cr = ZeroOrMore('w:cr') @@ -52,6 +58,61 @@ def add_drawing(self, inline_or_anchor): drawing.append(inline_or_anchor) return drawing + def add_comm(self, author, comment_part, initials, dtime, comment_text): + + comment = comment_part.add_comment(author, initials, dtime) + comment._add_p(comment_text) + # _r = self.add_r() + self.add_comment_reference(comment._id) + self.link_comment(comment._id) + + return comment + + def link_comment(self, _id): + rStart = OxmlElement('w:commentRangeStart') + rStart._id = _id + rEnd = OxmlElement('w:commentRangeEnd') + rEnd._id = _id + self.addprevious(rStart) + self.addnext(rEnd) + + def add_comment_reference(self, _id): + reference = OxmlElement('w:commentReference') + reference._id = _id + self.append(reference) + return reference + + def add_footnote_reference(self, _id): + rPr = self.get_or_add_rPr() + rstyle = rPr.get_or_add_rStyle() + rstyle.val = 'FootnoteReference' + reference = OxmlElement('w:footnoteReference') + reference._id = _id + self.append(reference) + return reference + + def add_footnoteRef(self): + ref = OxmlElement('w:footnoteRef') + self.append(ref) + + return ref + + def footnote_style(self): + rPr = self.get_or_add_rPr() + rstyle = rPr.get_or_add_rStyle() + rstyle.val = 'FootnoteReference' + + self.add_footnoteRef() + return self + + @property + def footnote_id(self): + _id = self.xpath('./w:footnoteReference/@w:id') + if len(_id) > 1 or len(_id) == 0: + return None + else: + return int(_id[0]) + def clear_content(self): """ Remove all child elements except the ```` element if present. @@ -60,6 +121,12 @@ def clear_content(self): for child in content_child_elms: self.remove(child) + def add_comment_reference(self, _id): + reference = OxmlElement('w:commentReference') + reference._id = _id + self.append(reference) + return reference + @property def style(self): """ @@ -96,6 +163,8 @@ def text(self): text += '\t' elif child.tag in (qn('w:br'), qn('w:cr')): text += '\n' + elif child.tag == qn('w:noBreakHyphen'): + text += '-' return text @text.setter @@ -103,6 +172,39 @@ def text(self, text): self.clear_content() _RunContentAppender.append_to_run_from_text(self, text) + def add_fldChar(self, fldCharType, fldLock=False, dirty=False): + if fldCharType not in ("begin", "end", "separate"): + return None + + fld_char = OxmlElement("w:fldChar") + fld_char.set(qn("w:fldCharType"), fldCharType) + if fldLock: + fld_char.set(qn("w:fldLock"), "true") + elif dirty: + fld_char.set(qn("w:fldLock"), "true") + self.append(fld_char) + return fld_char + + @property + def instr_text(self): + for child in list(self): + if child.tag.endswith("instrText"): + return child + return None + + @instr_text.setter + def instr_text(self, instr_text_val): + if self.instr_text is not None: + self._remove_instr_text() + + instr_text = OxmlElement("w:instrText") + instr_text.text = instr_text_val + self.append(instr_text) + + def _remove_instr_text(self): + for child in self.iterchildren("{*}instrText"): + self.remove(child) + class CT_Text(BaseOxmlElement): """ @@ -110,6 +212,14 @@ class CT_Text(BaseOxmlElement): """ +class CT_RPr(BaseOxmlElement): + rStyle = ZeroOrOne('w:rStyle') + + +class CT_RStyle(BaseOxmlElement): + val = RequiredAttribute('w:val', ST_String) + + class _RunContentAppender(object): """ Service object that knows how to translate a Python string into run @@ -119,6 +229,7 @@ class _RunContentAppender(object): appended. Likewise a newline or carriage return character ('\n', '\r') causes a ```` element to be appended. """ + def __init__(self, r): self._r = r self._bfr = [] diff --git a/docx/parts/comments.py b/docx/parts/comments.py new file mode 100644 index 000000000..03a045aea --- /dev/null +++ b/docx/parts/comments.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +import os + +from docx.opc.constants import CONTENT_TYPE as CT +from ..opc.packuri import PackURI + +from docx.oxml import parse_xml +from ..opc.part import XmlPart + +class CommentsPart(XmlPart): + """Definition of Comments Part""" + + @classmethod + def default(cls, package): + partname = PackURI("/word/comments.xml") + content_type = CT.WML_COMMENTS + element = parse_xml(cls._default_comments_xml()) + return cls(partname, content_type, element, package) + + @classmethod + def _default_comments_xml(cls): + path = os.path.join(os.path.split(__file__)[0], '..', 'templates', 'default-comments.xml') + with open(path, 'rb') as f: + xml_bytes = f.read() + return xml_bytes diff --git a/docx/parts/document.py b/docx/parts/document.py index 59d0b7a71..fe55eb3a6 100644 --- a/docx/parts/document.py +++ b/docx/parts/document.py @@ -11,6 +11,8 @@ from docx.parts.settings import SettingsPart from docx.parts.story import BaseStoryPart from docx.parts.styles import StylesPart +from docx.parts.comments import CommentsPart +from docx.parts.footnotes import FootnotesPart from docx.shape import InlineShapes from docx.shared import lazyproperty @@ -102,6 +104,8 @@ def numbering_part(self): numbering_part = NumberingPart.new() self.relate_to(numbering_part, RT.NUMBERING) return numbering_part + + def save(self, path_or_stream): """ @@ -152,3 +156,33 @@ def _styles_part(self): styles_part = StylesPart.default(self.package) self.relate_to(styles_part, RT.STYLES) return styles_part + + @lazyproperty + def comments_part(self): + """ + A |Comments| object providing read/write access to the core + properties of this document. + """ + # return self.package._comments_part + + @property + def _comments_part(self): + try: + return self.part_related_by(RT.COMMENTS) + except KeyError: + comments_part = CommentsPart.default(self) + self.relate_to(comments_part, RT.COMMENTS) + return comments_part + + @property + def _footnotes_part(self): + """ + |FootnotesPart| object related to this package. Creates + a default Comments part if one is not present. + """ + try: + return self.part_related_by(RT.FOOTNOTES) + except KeyError: + footnotes_part = FootnotesPart.default(self) + self.relate_to(footnotes_part, RT.FOOTNOTES) + return footnotes_part \ No newline at end of file diff --git a/docx/parts/footnotes.py b/docx/parts/footnotes.py new file mode 100644 index 000000000..67f29fb71 --- /dev/null +++ b/docx/parts/footnotes.py @@ -0,0 +1,26 @@ +from __future__ import absolute_import, division, print_function, unicode_literals + +from ..opc.constants import CONTENT_TYPE as CT +from ..opc.packuri import PackURI +from ..opc.part import XmlPart +from ..oxml import parse_xml + +import os + +class FootnotesPart(XmlPart): + """ + Definition of Footnotes Part + """ + @classmethod + def default(cls, package): + partname = PackURI("/word/footnotes.xml") + content_type = CT.WML_FOOTNOTES + element = parse_xml(cls._default_footnotes_xml()) + return cls(partname, content_type, element, package) + + @classmethod + def _default_footnotes_xml(cls): + path = os.path.join(os.path.split(__file__)[0], '..', 'templates', 'default-footnotes.xml') + with open(path, 'rb') as f: + xml_bytes = f.read() + return xml_bytes \ No newline at end of file diff --git a/docx/table.py b/docx/table.py index b3bc090fb..9801f8346 100644 --- a/docx/table.py +++ b/docx/table.py @@ -10,7 +10,7 @@ from .enum.style import WD_STYLE_TYPE from .oxml.simpletypes import ST_Merge from .shared import Inches, lazyproperty, Parented - +from .section import Section class Table(Parented): """ @@ -45,6 +45,9 @@ def add_row(self): return _Row(tr, self) @property + def section(self): + return Section(self._element._section, self.part) + @property def alignment(self): """ Read/write. A member of :ref:`WdRowAlignment` or None, specifying the diff --git a/docx/templates/default-comments.xml b/docx/templates/default-comments.xml new file mode 100644 index 000000000..4ceb12ea4 --- /dev/null +++ b/docx/templates/default-comments.xml @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/docx/templates/default-footnotes.xml b/docx/templates/default-footnotes.xml new file mode 100644 index 000000000..5dc12e66f --- /dev/null +++ b/docx/templates/default-footnotes.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/docx/text/comment.py b/docx/text/comment.py new file mode 100644 index 000000000..adc0110a1 --- /dev/null +++ b/docx/text/comment.py @@ -0,0 +1,23 @@ +from ..shared import Parented + +class Comment(Parented): + """[summary] + + :param Parented: [description] + :type Parented: [type] + """ + def __init__(self, com, parent): + super(Comment, self).__init__(parent) + self._com = self._element = self.element = com + + @property + def paragraph(self): + return self.element.paragraph + + @property + def text(self): + return self.element.paragraph.text + + @text.setter + def text(self, text): + self.element.paragraph.text = text \ No newline at end of file diff --git a/docx/text/font.py b/docx/text/font.py index 162832101..bdef5b808 100644 --- a/docx/text/font.py +++ b/docx/text/font.py @@ -197,6 +197,25 @@ def name(self, value): rPr.rFonts_ascii = value rPr.rFonts_hAnsi = value + @property + def theme(self): + """ + Get or set the typeface theme for this |Font| instance, causing the + text it controls to appear in the themed font, if a matching font is + found. |None| indicates the typeface is inherited from the style + hierarchy. + """ + rPr = self._element.rPr + if rPr is None: + return None + return rPr.rFonts_asciiTheme + + @theme.setter + def theme(self, value): + rPr = self._element.get_or_add_rPr() + rPr.rFonts_asciiTheme = value + rPr.rFonts_hAnsiTheme = value + @property def no_proof(self): """ diff --git a/docx/text/paragraph.py b/docx/text/paragraph.py index 4fb583b94..9bf676b92 100644 --- a/docx/text/paragraph.py +++ b/docx/text/paragraph.py @@ -13,6 +13,8 @@ from .run import Run from ..shared import Parented +from datetime import datetime +import re class Paragraph(Parented): """ @@ -38,7 +40,39 @@ def add_run(self, text=None, style=None): if style: run.style = style return run - + + def delete(self): + """ + delete the content of the paragraph + """ + self._p.getparent().remove(self._p) + self._p = self._element = None + + def add_comment(self, text, author='python-docx', initials='pd', dtime=None ,rangeStart=0, rangeEnd=0, comment_part=None): + if comment_part is None: + comment_part = self.part._comments_part.element + if dtime is None: + dtime = str( datetime.now() ).replace(' ', 'T') + comment = self._p.add_comm(author, comment_part, initials, dtime, text, rangeStart, rangeEnd) + + return comment + + def add_footnote(self, text): + footnotes_part = self.part._footnotes_part.element + footnote = self._p.add_fn(text, footnotes_part) + + return footnote + + def merge_paragraph(self, otherParagraph): + r_lst = otherParagraph.runs + self.append_runs(r_lst) + + def append_runs(self, runs): + self.add_run(' ') + for run in runs: + self._p.append(run._r) + + @property def alignment(self): """ @@ -93,6 +127,9 @@ def runs(self): return [Run(r, self) for r in self._p.r_lst] @property + def all_runs(self): + return [Run(r, self) for r in self._p.xpath('.//w:r[not(ancestor::w:r)]')] + @property def style(self): """ Read/Write. |_ParagraphStyle| object representing the style assigned @@ -131,6 +168,70 @@ def text(self): text += run.text return text + @property + def header_level(self): + ''' + input Paragraph Object + output Paragraph level in case of header or returns None + ''' + headerPattern = re.compile(".*Heading (\d+)$") + level = 0 + if headerPattern.match(self.style.name): + level = int(self.style.name.lower().split('heading')[-1].strip()) + return level + + @property + def NumId(self): + ''' + returns NumId val in case of paragraph has numbering + else: return None + ''' + try: + return self._p.pPr.numPr.numId.val + except: + return None + + @property + def list_lvl(self): + ''' + returns ilvl val in case of paragraph has a numbering level + else: return None + ''' + try: + return self._p.pPr.numPr.ilvl.val + except : + return None + + @property + def list_info(self): + ''' + returns tuple (has numbering info, numId value, ilvl value) + ''' + if self.NumId and self.list_lvl: + return True, self.NumId, self.list_lvl + else: + return False, 0, 0 + + @property + def is_heading(self): + return True if self.header_level else False + + @property + def full_text(self): + return u"".join([r.text for r in self.all_runs]) + + @property + def footnotes(self): + if self._p.footnote_ids is not None : + return True + else : + return False + + @property + def comments(self): + runs_comments = [run.comments for run in self.runs] + return [comment for comments in runs_comments for comment in comments] + @text.setter def text(self, text): self.clear() diff --git a/docx/text/run.py b/docx/text/run.py index 97d6da7db..0ed3e972a 100644 --- a/docx/text/run.py +++ b/docx/text/run.py @@ -5,6 +5,10 @@ """ from __future__ import absolute_import, print_function, unicode_literals +from datetime import datetime + +from docx.oxml.ns import qn +from docx.opc.part import * from ..enum.style import WD_STYLE_TYPE from ..enum.text import WD_BREAK @@ -12,6 +16,8 @@ from ..shape import InlineShape from ..shared import Parented +from .comment import Comment + class Run(Parented): """ @@ -21,6 +27,7 @@ class Run(Parented): not specified directly on the run and its effective value is taken from the style hierarchy. """ + def __init__(self, r, parent): super(Run, self).__init__(parent) self._r = self._element = self.element = r @@ -80,6 +87,14 @@ def add_text(self, text): t = self._r.add_t(text) return _Text(t) + def add_comment(self, text, author='python-docx', initials='pd', dtime=None): + comment_part = self.part._comments_part.element + if dtime is None: + dtime = str(datetime.now()).replace(' ', 'T') + comment = self._r.add_comm(author, comment_part, initials, dtime, text) + + return comment + @property def bold(self): """ @@ -181,11 +196,96 @@ def underline(self): def underline(self, value): self.font.underline = value + @property + def footnote(self): + _id = self._r.footnote_id + + if _id is not None: + footnotes_part = self._parent._parent.part._footnotes_part.element + footnote = footnotes_part.get_footnote_by_id(_id) + return footnote.paragraph.text + else: + return None + + @property + def is_hyperlink(self): + ''' + checks if the run is nested inside a hyperlink element + ''' + return self.element.getparent().tag.split('}')[1] == 'hyperlink' + + def get_hyperLink(self): + """ + returns the text of the hyperlink of the run in case of the run has a hyperlink + """ + document = self._parent._parent.document + parent = self.element.getparent() + linkText = '' + if self.is_hyperlink: + if parent.attrib.__contains__(qn('r:id')): + rId = parent.get(qn('r:id')) + linkText = document._part._rels[rId].target_ref + return linkText, True + elif parent.attrib.__contains__(qn('w:anchor')): + linkText = parent.get(qn('w:anchor')) + return linkText, False + else: + print('No Link in Hyperlink!') + print(self.text) + return '', False + else: + return 'None' + + @property + def comments(self): + comment_part = self._parent._parent.part._comments_part.element + comment_refs = self._element.findall(qn('w:commentReference')) + ids = [int(ref.get(qn('w:id'))) for ref in comment_refs] + coms = [com for com in comment_part if com._id in ids] + return [Comment(com, comment_part) for com in coms] + + def add_ole_object_to_run(self, ole_object_path): + """ + Add saved OLE Object in the disk to an run and retun the newly created relationship ID + Note: OLE Objects must be stored in the disc as `.bin` file + """ + reltype: str = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/oleObject" + pack_path: str = "/word/embeddings/" + ole_object_path.split("\\")[-1] + partname = PackURI(pack_path) + content_type: str = "application/vnd.openxmlformats-officedocument.oleObject" + + with open(ole_object_path, "rb") as f: + blob = f.read() + target_part = Part(partname=partname, content_type=content_type, blob=blob) + rel_id: str = self.part.rels._next_rId + self.part.rels.add_relationship(reltype=reltype, target=target_part, rId=rel_id) + return rel_id + + def add_fldChar(self, fldCharType, fldLock: bool = False, dirty: bool = False): + + fldChar = self._r.add_fldChar(fldCharType, fldLock, dirty) + return fldChar + + @property + def instr_text(self): + return self._r.instr_text + + @instr_text.setter + def instr_text(self, instr_text_val): + self._r.instr_text = instr_text_val + + def remove_instr_text(self): + if self.instr_text is None: + return None + else: + self._r._remove_instr_text() + class _Text(object): """ Proxy object wrapping ```` element. """ + def __init__(self, t_elm): super(_Text, self).__init__() self._t = t_elm diff --git a/setup.py b/setup.py index f0b3ef54d..ff8652647 100644 --- a/setup.py +++ b/setup.py @@ -26,13 +26,13 @@ def text_of(relpath): ).group(1) -NAME = 'python-docx' +NAME = 'bayoo-docx' VERSION = version DESCRIPTION = 'Create and update Microsoft Word .docx files.' KEYWORDS = 'docx office openxml word' -AUTHOR = 'Steve Canny' -AUTHOR_EMAIL = 'python-docx@googlegroups.com' -URL = 'https://github.com/python-openxml/python-docx' +AUTHOR = 'Obay Daba' +AUTHOR_EMAIL = 'ObayDaba96@googlegroups.com' +URL = 'https://github.com/BayooG/bayooo-docx' LICENSE = text_of('LICENSE') PACKAGES = find_packages(exclude=['tests', 'tests.*']) PACKAGE_DATA = {'docx': ['templates/*.xml', 'templates/*.docx']} @@ -58,7 +58,7 @@ def text_of(relpath): 'Topic :: Software Development :: Libraries' ] -LONG_DESCRIPTION = text_of('README.rst') + '\n\n' + text_of('HISTORY.rst') +LONG_DESCRIPTION = text_of('DESCRIPTION.rst') + '\n\n' + text_of('HISTORY.rst') params = {