Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
8278cbb
Add support for the table cell property gridSpan element, which is
apteryks Aug 4, 2014
0636018
Register a newly added CT_TcGridSpan (w:gridSpan) class/element.
apteryks Aug 4, 2014
b1b37ae
Add support for merging the cells of a single row.
apteryks Aug 4, 2014
90fbf3b
Add mergeStart, mergeStop args to merge_cells() to make it more
apteryks Aug 4, 2014
69f492b
Fix wrong index in merge_cells method (the gridSpan was set to the cell
apteryks Aug 8, 2014
b27fb26
Removed the previously added CT_TcPrGridSpan class, as we are now using
apteryks Aug 8, 2014
b11faac
Register CT_VMerge.
apteryks Aug 8, 2014
8a99e1f
Add ST_Merge class.
apteryks Aug 8, 2014
4a61381
Add vmerge methods to CT_Tc and CT_TcPr classes.
apteryks Aug 8, 2014
bd14d53
Detail the cell merge feature.
apteryks Aug 11, 2014
df16549
doc: Improve cell-merge formatting and add additionnal notes.
apteryks Aug 11, 2014
10d28fc
doc: add some details regarding merging and word behavior.
apteryks Aug 11, 2014
c98827a
doc: Improve Word specific behavior description.
apteryks Aug 12, 2014
98c0ab0
doc: cell-merge.rst retouches.
apteryks Aug 12, 2014
cb4130c
Add default value, 'continue', to CT_VMerge object.
apteryks Aug 13, 2014
d3cb255
doc: Add the information about the w:vMerge attribute having it's
apteryks Aug 13, 2014
b2e20f1
doc: Add some schemas to cell-merge.rst, removed web references.
apteryks Aug 13, 2014
482ecea
acpt: add cell-merge.feature
apteryks Aug 13, 2014
e5a1578
acpt: Refine the cell-merge feature scenarios
apteryks Aug 13, 2014
3ad233f
acpt: Add/modify steps for the cell-merge feature
apteryks Aug 13, 2014
bc4a1b0
acpt: Update some step names to reflect changes in the table steps.
apteryks Aug 13, 2014
4deb7f6
acpt: Bring back the 'Unsuprted merge of an alrdy merged area' scenario.
apteryks Aug 14, 2014
1dfb8fe
acpt: implement unsupported merge scenario and steps.
apteryks Aug 14, 2014
f28891d
acpt: row/column index cell property
apteryks Aug 15, 2014
5d415df
steps: Add steps for the row/column cell properties
apteryks Aug 15, 2014
17fdeab
tests: Add unit test for row/column index property
apteryks Aug 15, 2014
35fdd59
cell: Add column/row index property (read-only).
apteryks Aug 15, 2014
c4a9de1
Rename cell-merge.feature to cel-merge-feature
apteryks Aug 15, 2014
798ee84
Rename to cel-merge.feature
apteryks Aug 15, 2014
125fecb
doc: Add behavior of different tables cells merge attempt.
apteryks Aug 15, 2014
542758a
acpt: Reorganize in which files the scenario are defined & various fixes
apteryks Aug 15, 2014
e8fd91e
cell: Refine row/column index properties test and impl.
apteryks Aug 17, 2014
f89dc1b
cel-merge: Add support for horizontal merge.
apteryks Aug 17, 2014
8b865fc
doc: Removed the paragraph about python-docx using hMerge.
apteryks Aug 17, 2014
16dd3f8
doc: Update the Word specific behavior section of cell-merge.rst
apteryks Aug 18, 2014
8610703
merge: Add vertical merge support.
apteryks Aug 18, 2014
0f1813e
doc: Add more detail, CT_HMerge schema excerpt
apteryks Aug 19, 2014
0babb6c
merge: Register CT_HMerge element
apteryks Aug 19, 2014
dbab767
cell: Add hmerge property support
apteryks Aug 19, 2014
fe88588
test: add new fixture params to cell merge test
apteryks Aug 19, 2014
143e97c
acpt: update two-ways merge scenario and steps
apteryks Aug 19, 2014
98596d7
merge: add two-ways merge support
apteryks Aug 19, 2014
a3188f2
merge: cleanup, branch freeze pending integration.
apteryks Aug 19, 2014
1730940
merge: small cleanup in cell.py step file pending pull request.
apteryks Aug 19, 2014
6316d04
merge: cleanup cel-merge.feature from incompleted scenarios.
apteryks Aug 19, 2014
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
249 changes: 249 additions & 0 deletions docs/dev/analysis/features/cell-merge.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@

Table Cells Merge
=================

In Word, table cells can be merged with the following restrictions:

* Only rectangular selections are supported.
* If the to-be-merged selection contains previously merged cells, then that
selection must extend the contained merged cells area.

The area to be merged is determined by the two opposite corner cells of that
area. The to-be-merged area can span across multiple rows and/or columns.

For merging horizontally, the ``w:gridSpan`` table cell property of the
leftmost cell of the area to be merged is set to a value of type
``w:ST_DecimalNumber`` corresponding to the number of columns the cell
should span across. Only that leftmost cell is preserved; the other cells
of the merge selection are deleted. Note that having the ``w:gridSpan``
element is only required if there exists another table row using a
different column layout. When the same column layout is shared across all
the rows, then the ``w:gridSpan`` can be replaced by a ``w:tcW`` element
specifying the width of the column. For example, if the table consists of
just one row and we merge all of its cells, then only the leftmost cell is
kept, and its width is ajusted so that it equals the combined width of
the cells merged.

As an alternative to the previously described horizontal merging protocol,
``w:hMerge`` element can be used to identify the merged cells instead of
deleting them. This approach is prefered as it is non destructive and
thus maintains the structure of the table, which in turns allows for more
user-friendly cell addressing. This is the approach used by
the python-docx merge method.


For merging vertically, the ``w:vMerge`` table cell property of the
uppermost cell of the column is set to the value "restart" of type
``w:ST_Merge``. The following, lower cells included in the vertical merge
must have the ``w:vMerge`` element present in their cell property
(``w:TcPr``) element. Its value should be set to "continue", although it is
not necessary to explicitely define it, as it is the default value. A
vertical merge ends as soon as a cell ``w:TcPr`` element lacks the
``w:vMerge`` element. Similarly to the ``w:gridSpan`` element, the
``w:vMerge`` elements are only required when the table's layout is not
uniform across its different columns. In the case it is, only the topmost
cell is kept; the other lower cells in the merged area are deleted along
with their ``w:vMerge`` elements and the ``w:trHeight`` table row property
is used to specify the combined height of the merged cells.


Word specific behavior
~~~~~~~~~~~~~~~~~~~~~~

Word cannot access the columns of a table if two or more cells from that
table have been horizontally merged. Similarly, Word cannot access the rows
of a table if two or more cells from that table have been vertically merged.

Horizontally merged cells other than the leftmost cell are deleted and thus
can no longer be accessed.

Vertically merged cells marked by ``w:vMerge=continue`` are no longer
accessible from Word. An exception with the message "The member of the
collection does not exist" is raised.

Word reports the length of a row or column containing merged cells as the
visual length. For example, the reported length of a 3 columns rows which
two first cells have been merged would be 2. Similarly, the reported length of
a 2 rows column which two cells have been merged would be 1.

Word resizes a table when a cell is refered by an out-of-bounds row index.
If the column identifier is out of bounds, an exception is raised.

An exception is raised when attempting to merge cells from different tables.


python-docx API refinements over Word's
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Addressing some of the Word API deficiencies when dealing with merged cells,
the following new features were introduced:

* The length of any rows or columns remain available for report even when two
or more cells have been merged. The length is reported as the count of all
the normal (unmerged) cells, plus all the *master* merged cells. By *master*
merged cells, we understand the leftmost cell of an horizontally merged
area, the topmost cell of a vertically merged area, or the topleftmost cell
of two-ways merged area.

* The same logic is applied to filter the iterable cells in a _ColumnCells or
_RowCells cells collection and a restricted access error message is written
when trying to access visually hidden, non master merged cells.

* The smart filtering of hidden merged cells, dubbed *visual grid* can be
turned off to gain access to cells which would normally be restricted,
either via the ``Table.cell`` method's third argument, or by setting the
``visual_grid`` static property of a ``_RowCells`` or ``_ColumnsCell``
instance to *False*.


Candidate protocol -- cell.merge()
----------------------------------

The following interactive session demonstrates the protocol for merging table
cells. The capability of reporting the length of merged cells collection is
also demonstrated::

>>> table = doc.add_table(5, 5)
>>> table.cell(0, 0).merge(table.cell(3, 3))
>>> len(table.columns[2].cells)
1
>>> cells = table.columns[2].cells
>>> cells.visual_grid = False
>>> len(cells)
5

Specimen XML
------------

.. highlight:: xml

A 3 x 3 table where an area defined by the 2 x 2 topleft cells has been
merged, demonstrating the combined use of the ``w:gridSpan`` as well as the
``w:vMerge`` elements, as produced by Word::

<w:tbl>
<w:tblPr>
<w:tblW w:w="0" w:type="auto" />
</w:tblPr>
<w:tblGrid>
<w:gridCol w:w="3192" />
<w:gridCol w:w="3192" />
<w:gridCol w:w="3192" />
</w:tblGrid>
<w:tr>
<w:tc>
<w:tcPr>
<w:tcW w:w="6384" w:type="dxa" />
<w:gridSpan w:val="2" />
<w:vMerge w:val="restart" />
</w:tcPr>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3192" w:type="dxa" />
</w:tcPr>
</w:tc>
</w:tr>
<w:tr>
<w:tc>
<w:tcPr>
<w:tcW w:w="6384" w:type="dxa" />
<w:gridSpan w:val="2" />
<w:vMerge />
</w:tcPr>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3192" w:type="dxa" />
</w:tcPr>
</w:tc>
</w:tr>
<w:tr>
<w:tc>
<w:tcPr>
<w:tcW w:w="3192" w:type="dxa" />
</w:tcPr>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3192" w:type="dxa" />
</w:tcPr>
</w:tc>
<w:tc>
<w:tcPr>
<w:tcW w:w="3192" w:type="dxa" />
</w:tcPr>
</w:tc>
</w:tr>
</w:tbl>


Schema excerpt
--------------

.. highlight:: xml

::

<xsd:complexType name="CT_TcPr"> <!-- denormalized -->
<xsd:sequence>
<xsd:element name="cnfStyle" type="CT_Cnf" minOccurs="0"/>
<xsd:element name="tcW" type="CT_TblWidth" minOccurs="0"/>
<xsd:element name="gridSpan" type="CT_DecimalNumber" minOccurs="0"/>
<xsd:element name="hMerge" type="CT_HMerge" minOccurs="0"/>
<xsd:element name="vMerge" type="CT_VMerge" minOccurs="0"/>
<xsd:element name="tcBorders" type="CT_TcBorders" minOccurs="0"/>
<xsd:element name="shd" type="CT_Shd" minOccurs="0"/>
<xsd:element name="noWrap" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="tcMar" type="CT_TcMar" minOccurs="0"/>
<xsd:element name="textDirection" type="CT_TextDirection" minOccurs="0"/>
<xsd:element name="tcFitText" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="vAlign" type="CT_VerticalJc" minOccurs="0"/>
<xsd:element name="hideMark" type="CT_OnOff" minOccurs="0"/>
<xsd:element name="headers" type="CT_Headers" minOccurs="0"/>
<xsd:choice minOccurs="0"/>
<xsd:element name="cellIns" type="CT_TrackChange"/>
<xsd:element name="cellDel" type="CT_TrackChange"/>
<xsd:element name="cellMerge" type="CT_CellMergeTrackChange"/>
</xsd:choice>
<xsd:element name="tcPrChange" type="CT_TcPrChange" minOccurs="0"/>
</xsd:sequence>
</xsd:complexType>

<xsd:complexType name="CT_DecimalNumber">
<xsd:attribute name="val" type="ST_DecimalNumber" use="required"/>
</xsd:complexType>

<xsd:simpleType name="ST_DecimalNumber">
<xsd:restriction base="xsd:integer"/>
</xsd:simpleType>

<xsd:complexType name="CT_VMerge">
<xsd:attribute name="val" type="ST_Merge"/>
</xsd:complexType>

<xsd:complexType name="CT_HMerge">
<xsd:attribute name="val" type="ST_Merge"/>
</xsd:complexType>

<xsd:simpleType name="ST_Merge">
<xsd:restriction base="xsd:string">
<xsd:enumeration value="continue"/>
<xsd:enumeration value="restart"/>
</xsd:restriction>
</xsd:simpleType>


Ressources
----------

* `Cell.Merge Method on MSDN`_

.. _`Cell.Merge Method on MSDN`:
http://msdn.microsoft.com/en-us/library/office/ff821310%28v=office.15%29.aspx

Relevant sections in the ISO Spec
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
* 17.4.17 gridSpan (Grid Columns Spanned by Current Table Cell)
* 17.4.84 vMerge (Vertically Merged Cell)
* 17.18.57 ST_Merge (Merged Cell Type)
1 change: 1 addition & 0 deletions docs/dev/analysis/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Feature Analysis
features/table
features/table-props
features/table-cell
features/cell-merge
features/par-alignment
features/run-content
features/numbering
Expand Down
5 changes: 4 additions & 1 deletion docx/oxml/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -115,9 +115,10 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):

from docx.oxml.table import (
CT_Row, CT_Tbl, CT_TblGrid, CT_TblGridCol, CT_TblLayoutType, CT_TblPr,
CT_TblWidth, CT_Tc, CT_TcPr
CT_TblWidth, CT_Tc, CT_TcPr, CT_HMerge, CT_VMerge
)
register_element_cls('w:gridCol', CT_TblGridCol)
register_element_cls('w:gridSpan', CT_DecimalNumber)
register_element_cls('w:tbl', CT_Tbl)
register_element_cls('w:tblGrid', CT_TblGrid)
register_element_cls('w:tblLayout', CT_TblLayoutType)
Expand All @@ -127,6 +128,8 @@ def OxmlElement(nsptag_str, attrs=None, nsdecls=None):
register_element_cls('w:tcPr', CT_TcPr)
register_element_cls('w:tcW', CT_TblWidth)
register_element_cls('w:tr', CT_Row)
register_element_cls('w:hMerge', CT_HMerge)
register_element_cls('w:vMerge', CT_VMerge)

from docx.oxml.text import (
CT_Br, CT_Jc, CT_P, CT_PPr, CT_R, CT_RPr, CT_Text, CT_Underline
Expand Down
11 changes: 11 additions & 0 deletions docx/oxml/simpletypes.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,17 @@ class ST_DrawingElementId(XsdUnsignedInt):
pass


class ST_Merge(XsdString):

@classmethod
def validate(cls, value):
cls.validate_string(value)
valid_values = ('continue', 'restart')
if value not in valid_values:
raise ValueError(
"must be one of %s, got '%s'" % (valid_values, value)
)

class ST_OnOff(XsdBoolean):

@classmethod
Expand Down
Loading