#
tokens: 23819/50000 3/35 files (page 2/2)
lines: off (toggle) GitHub
raw markdown copy
This is page 2 of 2. Use http://codebase.md/gongrzhe/office-word-mcp-server?page={x} to view the full context.

# Directory Structure

```
├── __init__.py
├── .gitignore
├── Dockerfile
├── LICENSE
├── mcp-config.json
├── office_word_mcp_server
│   └── __init__.py
├── pyproject.toml
├── README.md
├── RENDER_DEPLOYMENT.md
├── requirements.txt
├── setup_mcp.py
├── smithery.yaml
├── test_formatting.py
├── tests
│   └── test_convert_to_pdf.py
├── uv.lock
├── word_document_server
│   ├── __init__.py
│   ├── core
│   │   ├── __init__.py
│   │   ├── comments.py
│   │   ├── footnotes.py
│   │   ├── protection.py
│   │   ├── styles.py
│   │   ├── tables.py
│   │   └── unprotect.py
│   ├── main.py
│   ├── tools
│   │   ├── __init__.py
│   │   ├── comment_tools.py
│   │   ├── content_tools.py
│   │   ├── document_tools.py
│   │   ├── extended_document_tools.py
│   │   ├── footnote_tools.py
│   │   ├── format_tools.py
│   │   └── protection_tools.py
│   └── utils
│       ├── __init__.py
│       ├── document_utils.py
│       ├── extended_document_utils.py
│       └── file_utils.py
└── word_mcp_server.py
```

# Files

--------------------------------------------------------------------------------
/word_document_server/core/tables.py:
--------------------------------------------------------------------------------

```python
"""
Table-related operations for Word Document Server.
"""
from docx.oxml.shared import OxmlElement, qn
from docx.oxml.ns import nsdecls
from docx.oxml import parse_xml
from docx.shared import RGBColor, Inches, Cm, Pt
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.table import WD_CELL_VERTICAL_ALIGNMENT


def set_cell_border(cell, **kwargs):
    """
    Set cell border properties.
    
    Args:
        cell: The cell to modify
        **kwargs: Border properties (top, bottom, left, right, val, color)
    """
    tc = cell._tc
    tcPr = tc.get_or_add_tcPr()
    
    # Create border elements
    for key, value in kwargs.items():
        if key in ['top', 'left', 'bottom', 'right']:
            tag = 'w:{}'.format(key)
            
            element = OxmlElement(tag)
            element.set(qn('w:val'), kwargs.get('val', 'single'))
            element.set(qn('w:sz'), kwargs.get('sz', '4'))
            element.set(qn('w:space'), kwargs.get('space', '0'))
            element.set(qn('w:color'), kwargs.get('color', 'auto'))
            
            tcBorders = tcPr.first_child_found_in("w:tcBorders")
            if tcBorders is None:
                tcBorders = OxmlElement('w:tcBorders')
                tcPr.append(tcBorders)
                
            tcBorders.append(element)


def apply_table_style(table, has_header_row=False, border_style=None, shading=None):
    """
    Apply formatting to a table.
    
    Args:
        table: The table to format
        has_header_row: If True, formats the first row as a header
        border_style: Style for borders ('none', 'single', 'double', 'thick')
        shading: 2D list of cell background colors (by row and column)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Format header row if requested
        if has_header_row and table.rows:
            header_row = table.rows[0]
            for cell in header_row.cells:
                for paragraph in cell.paragraphs:
                    if paragraph.runs:
                        for run in paragraph.runs:
                            run.bold = True
        
        # Apply border style if specified
        if border_style:
            val_map = {
                'none': 'nil',
                'single': 'single',
                'double': 'double',
                'thick': 'thick'
            }
            val = val_map.get(border_style.lower(), 'single')
            
            # Apply to all cells
            for row in table.rows:
                for cell in row.cells:
                    set_cell_border(
                        cell,
                        top=True,
                        bottom=True,
                        left=True,
                        right=True,
                        val=val,
                        color="000000"
                    )
        
        # Apply cell shading if specified
        if shading:
            for i, row_colors in enumerate(shading):
                if i >= len(table.rows):
                    break
                for j, color in enumerate(row_colors):
                    if j >= len(table.rows[i].cells):
                        break
                    try:
                        # Apply shading to cell
                        cell = table.rows[i].cells[j]
                        shading_elm = parse_xml(f'<w:shd {nsdecls("w")} w:fill="{color}"/>')
                        cell._tc.get_or_add_tcPr().append(shading_elm)
                    except:
                        # Skip if color format is invalid
                        pass
        
        return True
    except Exception:
        return False


def copy_table(source_table, target_doc):
    """
    Copy a table from one document to another.
    
    Args:
        source_table: The table to copy
        target_doc: The document to copy the table to
        
    Returns:
        The new table in the target document
    """
    # Create a new table with the same dimensions
    new_table = target_doc.add_table(rows=len(source_table.rows), cols=len(source_table.columns))
    
    # Try to apply the same style
    try:
        if source_table.style:
            new_table.style = source_table.style
    except:
        # Fall back to default grid style
        try:
            new_table.style = 'Table Grid'
        except:
            pass
    
    # Copy cell contents
    for i, row in enumerate(source_table.rows):
        for j, cell in enumerate(row.cells):
            for paragraph in cell.paragraphs:
                if paragraph.text:
                    new_table.cell(i, j).text = paragraph.text
    
    return new_table


def set_cell_shading(cell, fill_color=None, pattern="clear", pattern_color="auto"):
    """
    Apply shading/filling to a table cell.
    
    Args:
        cell: The table cell to format
        fill_color: Background color (hex string like "FF0000" or RGBColor)
        pattern: Shading pattern ("clear", "solid", "pct10", "pct20", etc.)
        pattern_color: Pattern color for patterned fills
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Get or create table cell properties
        tc_pr = cell._tc.get_or_add_tcPr()
        
        # Remove existing shading
        existing_shd = tc_pr.find(qn('w:shd'))
        if existing_shd is not None:
            tc_pr.remove(existing_shd)
        
        # Create shading element
        shd_attrs = {
            'w:val': pattern,
            'w:color': pattern_color if pattern_color != "auto" else "auto"
        }
        
        # Set fill color
        if fill_color:
            if isinstance(fill_color, str):
                # Hex color string - remove # if present
                fill_color = fill_color.lstrip('#').upper()
                if len(fill_color) == 6:  # Valid hex color
                    shd_attrs['w:fill'] = fill_color
            elif isinstance(fill_color, RGBColor):
                # RGBColor object
                hex_color = f"{fill_color.r:02X}{fill_color.g:02X}{fill_color.b:02X}"
                shd_attrs['w:fill'] = hex_color
        
        # Build XML string
        attr_str = ' '.join([f'{k}="{v}"' for k, v in shd_attrs.items()])
        shd_xml = f'<w:shd {nsdecls("w")} {attr_str}/>'
        
        # Parse and append shading element
        shading_elm = parse_xml(shd_xml)
        tc_pr.append(shading_elm)
        
        return True
        
    except Exception as e:
        print(f"Error setting cell shading: {e}")
        return False


def apply_alternating_row_shading(table, color1="FFFFFF", color2="F2F2F2"):
    """
    Apply alternating row colors for better readability.
    
    Args:
        table: The table to format
        color1: Color for odd rows (hex string)
        color2: Color for even rows (hex string)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        for i, row in enumerate(table.rows):
            fill_color = color1 if i % 2 == 0 else color2
            for cell in row.cells:
                set_cell_shading(cell, fill_color=fill_color)
        return True
    except Exception as e:
        print(f"Error applying alternating row shading: {e}")
        return False


def highlight_header_row(table, header_color="4472C4", text_color="FFFFFF"):
    """
    Apply special shading to header row.
    
    Args:
        table: The table to format
        header_color: Background color for header (hex string)
        text_color: Text color for header (hex string)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        if table.rows:
            for cell in table.rows[0].cells:
                # Apply background shading
                set_cell_shading(cell, fill_color=header_color)
                
                # Apply text formatting
                for paragraph in cell.paragraphs:
                    for run in paragraph.runs:
                        run.bold = True
                        if text_color and text_color != "auto":
                            # Convert hex to RGB
                            try:
                                text_color = text_color.lstrip('#')
                                r = int(text_color[0:2], 16)
                                g = int(text_color[2:4], 16)
                                b = int(text_color[4:6], 16)
                                run.font.color.rgb = RGBColor(r, g, b)
                            except:
                                pass  # Skip if color format is invalid
        return True
    except Exception as e:
        print(f"Error highlighting header row: {e}")
        return False


def set_cell_shading_by_position(table, row_index, col_index, fill_color, pattern="clear"):
    """
    Apply shading to a specific cell by row/column position.
    
    Args:
        table: The table containing the cell
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        fill_color: Background color (hex string)
        pattern: Shading pattern
        
    Returns:
        True if successful, False otherwise
    """
    try:
        if (0 <= row_index < len(table.rows) and 
            0 <= col_index < len(table.rows[row_index].cells)):
            cell = table.rows[row_index].cells[col_index]
            return set_cell_shading(cell, fill_color=fill_color, pattern=pattern)
        else:
            return False
    except Exception as e:
        print(f"Error setting cell shading by position: {e}")
        return False


def merge_cells(table, start_row, start_col, end_row, end_col):
    """
    Merge cells in a rectangular area.
    
    Args:
        table: The table containing cells to merge
        start_row: Starting row index (0-based)
        start_col: Starting column index (0-based)
        end_row: Ending row index (0-based, inclusive)
        end_col: Ending column index (0-based, inclusive)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Validate indices
        if (start_row < 0 or start_col < 0 or end_row < 0 or end_col < 0 or
            start_row >= len(table.rows) or end_row >= len(table.rows) or
            start_row > end_row or start_col > end_col):
            return False
        
        # Check if all rows have enough columns
        for row_idx in range(start_row, end_row + 1):
            if (start_col >= len(table.rows[row_idx].cells) or 
                end_col >= len(table.rows[row_idx].cells)):
                return False
        
        # Get the start and end cells
        start_cell = table.cell(start_row, start_col)
        end_cell = table.cell(end_row, end_col)
        
        # Merge the cells
        start_cell.merge(end_cell)
        
        return True
        
    except Exception as e:
        print(f"Error merging cells: {e}")
        return False


def merge_cells_horizontal(table, row_index, start_col, end_col):
    """
    Merge cells horizontally in a single row.
    
    Args:
        table: The table containing cells to merge
        row_index: Row index (0-based)
        start_col: Starting column index (0-based)
        end_col: Ending column index (0-based, inclusive)
        
    Returns:
        True if successful, False otherwise
    """
    return merge_cells(table, row_index, start_col, row_index, end_col)


def merge_cells_vertical(table, col_index, start_row, end_row):
    """
    Merge cells vertically in a single column.
    
    Args:
        table: The table containing cells to merge
        col_index: Column index (0-based)
        start_row: Starting row index (0-based)
        end_row: Ending row index (0-based, inclusive)
        
    Returns:
        True if successful, False otherwise
    """
    return merge_cells(table, start_row, col_index, end_row, col_index)


def set_cell_alignment(cell, horizontal="left", vertical="top"):
    """
    Set text alignment within a cell.
    
    Args:
        cell: The table cell to format
        horizontal: Horizontal alignment ("left", "center", "right", "justify")
        vertical: Vertical alignment ("top", "center", "bottom")
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Set horizontal alignment for all paragraphs in the cell
        for paragraph in cell.paragraphs:
            if horizontal.lower() == "center":
                paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER
            elif horizontal.lower() == "right":
                paragraph.alignment = WD_ALIGN_PARAGRAPH.RIGHT
            elif horizontal.lower() == "justify":
                paragraph.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
            else:  # default to left
                paragraph.alignment = WD_ALIGN_PARAGRAPH.LEFT
        
        # Set vertical alignment for the cell using XML manipulation
        tc_pr = cell._tc.get_or_add_tcPr()
        
        # Remove existing vertical alignment
        existing_valign = tc_pr.find(qn('w:vAlign'))
        if existing_valign is not None:
            tc_pr.remove(existing_valign)
        
        # Create vertical alignment element
        valign_element = OxmlElement('w:vAlign')
        if vertical.lower() == "center":
            valign_element.set(qn('w:val'), 'center')
        elif vertical.lower() == "bottom":
            valign_element.set(qn('w:val'), 'bottom')
        else:  # default to top
            valign_element.set(qn('w:val'), 'top')
        
        tc_pr.append(valign_element)
        
        return True
        
    except Exception as e:
        print(f"Error setting cell alignment: {e}")
        return False


def set_cell_alignment_by_position(table, row_index, col_index, horizontal="left", vertical="top"):
    """
    Set text alignment for a specific cell by position.
    
    Args:
        table: The table containing the cell
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        horizontal: Horizontal alignment ("left", "center", "right", "justify")
        vertical: Vertical alignment ("top", "center", "bottom")
        
    Returns:
        True if successful, False otherwise
    """
    try:
        if (0 <= row_index < len(table.rows) and 
            0 <= col_index < len(table.rows[row_index].cells)):
            cell = table.rows[row_index].cells[col_index]
            return set_cell_alignment(cell, horizontal, vertical)
        else:
            return False
    except Exception as e:
        print(f"Error setting cell alignment by position: {e}")
        return False


def set_table_alignment(table, horizontal="left", vertical="top"):
    """
    Set text alignment for all cells in a table.
    
    Args:
        table: The table to format
        horizontal: Horizontal alignment ("left", "center", "right", "justify")
        vertical: Vertical alignment ("top", "center", "bottom")
        
    Returns:
        True if successful, False otherwise
    """
    try:
        for row in table.rows:
            for cell in row.cells:
                set_cell_alignment(cell, horizontal, vertical)
        return True
    except Exception as e:
        print(f"Error setting table alignment: {e}")
        return False


def set_column_width(table, col_index, width, width_type="dxa"):
    """
    Set the width of a specific column in a table.
    
    Args:
        table: The table to modify
        col_index: Column index (0-based)
        width: Column width value
        width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Validate column index
        if col_index < 0 or col_index >= len(table.columns):
            return False
        
        # Convert width based on type
        if width_type == "dxa":
            # DXA units (twentieths of a point)
            if isinstance(width, (int, float)):
                width_value = str(int(width * 20))
            else:
                width_value = str(width)
        elif width_type == "pct":
            # Percentage (multiply by 50 for Word format)
            if isinstance(width, (int, float)):
                width_value = str(int(width * 50))
            else:
                width_value = str(width)
        else:
            width_value = str(width)
        
        # Iterate through all rows and set width for cells in the specified column
        for row in table.rows:
            if col_index < len(row.cells):
                cell = row.cells[col_index]
                tc_pr = cell._tc.get_or_add_tcPr()
                
                # Remove existing width
                existing_width = tc_pr.find(qn('w:tcW'))
                if existing_width is not None:
                    tc_pr.remove(existing_width)
                
                # Create new width element
                width_element = OxmlElement('w:tcW')
                width_element.set(qn('w:w'), width_value)
                width_element.set(qn('w:type'), width_type)
                
                tc_pr.append(width_element)
        
        return True
        
    except Exception as e:
        print(f"Error setting column width: {e}")
        return False


def set_column_width_by_position(table, col_index, width, width_type="dxa"):
    """
    Set the width of a specific column by position.
    
    Args:
        table: The table containing the column
        col_index: Column index (0-based)
        width: Column width value
        width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
        
    Returns:
        True if successful, False otherwise
    """
    return set_column_width(table, col_index, width, width_type)


def set_column_widths(table, widths, width_type="dxa"):
    """
    Set widths for multiple columns in a table.
    
    Args:
        table: The table to modify
        widths: List of width values for each column
        width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
        
    Returns:
        True if successful, False otherwise
    """
    try:
        for col_index, width in enumerate(widths):
            if col_index >= len(table.columns):
                break
            if not set_column_width(table, col_index, width, width_type):
                return False
        return True
    except Exception as e:
        print(f"Error setting column widths: {e}")
        return False


def set_table_width(table, width, width_type="dxa"):
    """
    Set the overall width of a table.
    
    Args:
        table: The table to modify
        width: Table width value
        width_type: Width type ("dxa" for points*20, "pct" for percentage*50, "auto")
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Convert width based on type
        if width_type == "dxa":
            # DXA units (twentieths of a point)
            if isinstance(width, (int, float)):
                width_value = str(int(width * 20))
            else:
                width_value = str(width)
        elif width_type == "pct":
            # Percentage (multiply by 50 for Word format)
            if isinstance(width, (int, float)):
                width_value = str(int(width * 50))
            else:
                width_value = str(width)
        else:
            width_value = str(width)
        
        # Get table element and properties
        tbl = table._tbl
        
        # Get or create table properties
        tbl_pr = tbl.find(qn('w:tblPr'))
        if tbl_pr is None:
            tbl_pr = OxmlElement('w:tblPr')
            tbl.insert(0, tbl_pr)
        
        # Remove existing table width
        existing_width = tbl_pr.find(qn('w:tblW'))
        if existing_width is not None:
            tbl_pr.remove(existing_width)
        
        # Create new table width element
        width_element = OxmlElement('w:tblW')
        width_element.set(qn('w:w'), width_value)
        width_element.set(qn('w:type'), width_type)
        
        tbl_pr.append(width_element)
        
        return True
        
    except Exception as e:
        print(f"Error setting table width: {e}")
        return False


def auto_fit_table(table):
    """
    Set table to auto-fit columns based on content.
    
    Args:
        table: The table to modify
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Get table element and properties
        tbl = table._tbl
        
        # Get or create table properties
        tbl_pr = tbl.find(qn('w:tblPr'))
        if tbl_pr is None:
            tbl_pr = OxmlElement('w:tblPr')
            tbl.insert(0, tbl_pr)
        
        # Remove existing layout
        existing_layout = tbl_pr.find(qn('w:tblLayout'))
        if existing_layout is not None:
            tbl_pr.remove(existing_layout)
        
        # Create auto layout element
        layout_element = OxmlElement('w:tblLayout')
        layout_element.set(qn('w:type'), 'autofit')
        
        tbl_pr.append(layout_element)
        
        # Set all column widths to auto
        for col_index in range(len(table.columns)):
            set_column_width(table, col_index, 0, "auto")
        
        return True
        
    except Exception as e:
        print(f"Error setting auto-fit table: {e}")
        return False


def format_cell_text(cell, text_content=None, bold=None, italic=None, underline=None, 
                    color=None, font_size=None, font_name=None):
    """
    Format text within a table cell.
    
    Args:
        cell: The table cell to format
        text_content: Optional new text content for the cell
        bold: Set text bold (True/False)
        italic: Set text italic (True/False)
        underline: Set text underlined (True/False)
        color: Text color (hex string like "FF0000" or color name)
        font_size: Font size in points
        font_name: Font name/family
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Set text content if provided
        if text_content is not None:
            cell.text = str(text_content)
        
        # Apply formatting to all paragraphs and runs in the cell
        for paragraph in cell.paragraphs:
            for run in paragraph.runs:
                if bold is not None:
                    run.bold = bold
                if italic is not None:
                    run.italic = italic
                if underline is not None:
                    run.underline = underline
                    
                if font_size is not None:
                    from docx.shared import Pt
                    run.font.size = Pt(font_size)
                    
                if font_name is not None:
                    run.font.name = font_name
                    
                if color is not None:
                    from docx.shared import RGBColor
                    # Define common RGB colors
                    color_map = {
                        'red': RGBColor(255, 0, 0),
                        'blue': RGBColor(0, 0, 255),
                        'green': RGBColor(0, 128, 0),
                        'yellow': RGBColor(255, 255, 0),
                        'black': RGBColor(0, 0, 0),
                        'gray': RGBColor(128, 128, 128),
                        'grey': RGBColor(128, 128, 128),
                        'white': RGBColor(255, 255, 255),
                        'purple': RGBColor(128, 0, 128),
                        'orange': RGBColor(255, 165, 0)
                    }
                    
                    try:
                        if color.lower() in color_map:
                            # Use predefined RGB color
                            run.font.color.rgb = color_map[color.lower()]
                        elif color.startswith('#'):
                            # Hex color string
                            hex_color = color.lstrip('#')
                            if len(hex_color) == 6:
                                r = int(hex_color[0:2], 16)
                                g = int(hex_color[2:4], 16)
                                b = int(hex_color[4:6], 16)
                                run.font.color.rgb = RGBColor(r, g, b)
                        else:
                            # Try hex without #
                            if len(color) == 6:
                                r = int(color[0:2], 16)
                                g = int(color[2:4], 16)
                                b = int(color[4:6], 16)
                                run.font.color.rgb = RGBColor(r, g, b)
                    except Exception:
                        # If color parsing fails, default to black
                        run.font.color.rgb = RGBColor(0, 0, 0)
        
        return True
        
    except Exception as e:
        print(f"Error formatting cell text: {e}")
        return False


def format_cell_text_by_position(table, row_index, col_index, text_content=None, 
                                 bold=None, italic=None, underline=None, color=None, 
                                 font_size=None, font_name=None):
    """
    Format text in a specific table cell by position.
    
    Args:
        table: The table containing the cell
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        text_content: Optional new text content for the cell
        bold: Set text bold (True/False)
        italic: Set text italic (True/False)
        underline: Set text underlined (True/False)
        color: Text color (hex string or color name)
        font_size: Font size in points
        font_name: Font name/family
        
    Returns:
        True if successful, False otherwise
    """
    try:
        if (0 <= row_index < len(table.rows) and 
            0 <= col_index < len(table.rows[row_index].cells)):
            cell = table.rows[row_index].cells[col_index]
            return format_cell_text(cell, text_content, bold, italic, underline, 
                                   color, font_size, font_name)
        else:
            return False
    except Exception as e:
        print(f"Error formatting cell text by position: {e}")
        return False


def set_cell_padding(cell, top=None, bottom=None, left=None, right=None, unit="dxa"):
    """
    Set padding/margins for a table cell.
    
    Args:
        cell: The table cell to format
        top: Top padding value
        bottom: Bottom padding value
        left: Left padding value
        right: Right padding value
        unit: Unit type ("dxa" for twentieths of a point, "pct" for percentage)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        # Get or create table cell properties
        tc_pr = cell._tc.get_or_add_tcPr()
        
        # Remove existing margins
        existing_margins = tc_pr.find(qn('w:tcMar'))
        if existing_margins is not None:
            tc_pr.remove(existing_margins)
        
        # Create margins element if any padding is specified
        if any(p is not None for p in [top, bottom, left, right]):
            margins_element = OxmlElement('w:tcMar')
            
            # Add individual margin elements
            margin_sides = {
                'w:top': top,
                'w:bottom': bottom,
                'w:left': left,
                'w:right': right
            }
            
            for side, value in margin_sides.items():
                if value is not None:
                    margin_el = OxmlElement(side)
                    if unit == "dxa":
                        # DXA units (twentieths of a point)
                        margin_el.set(qn('w:w'), str(int(value * 20)))
                        margin_el.set(qn('w:type'), 'dxa')
                    elif unit == "pct":
                        # Percentage
                        margin_el.set(qn('w:w'), str(int(value * 50)))
                        margin_el.set(qn('w:type'), 'pct')
                    else:
                        # Default to DXA
                        margin_el.set(qn('w:w'), str(int(value * 20)))
                        margin_el.set(qn('w:type'), 'dxa')
                    
                    margins_element.append(margin_el)
            
            tc_pr.append(margins_element)
        
        return True
        
    except Exception as e:
        print(f"Error setting cell padding: {e}")
        return False


def set_cell_padding_by_position(table, row_index, col_index, top=None, bottom=None, 
                                left=None, right=None, unit="dxa"):
    """
    Set padding for a specific table cell by position.
    
    Args:
        table: The table containing the cell
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        top: Top padding value
        bottom: Bottom padding value
        left: Left padding value
        right: Right padding value
        unit: Unit type ("dxa" for twentieths of a point, "pct" for percentage)
        
    Returns:
        True if successful, False otherwise
    """
    try:
        if (0 <= row_index < len(table.rows) and 
            0 <= col_index < len(table.rows[row_index].cells)):
            cell = table.rows[row_index].cells[col_index]
            return set_cell_padding(cell, top, bottom, left, right, unit)
        else:
            return False
    except Exception as e:
        print(f"Error setting cell padding by position: {e}")
        return False

```

--------------------------------------------------------------------------------
/word_document_server/core/footnotes.py:
--------------------------------------------------------------------------------

```python
"""
Consolidated footnote functionality for Word documents.
This module combines all footnote implementations with proper namespace handling and Word compliance.
"""

import os
import zipfile
import tempfile
from typing import Optional, Tuple, Dict, Any, List
from lxml import etree
from docx import Document
from docx.oxml.ns import qn

# Namespace definitions
W_NS = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'
R_NS = 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'
CT_NS = 'http://schemas.openxmlformats.org/package/2006/content-types'
REL_NS = 'http://schemas.openxmlformats.org/package/2006/relationships'

# Constants
RESERVED_FOOTNOTE_IDS = {-1, 0, 1}  # Reserved for separators and Word internals
MIN_FOOTNOTE_ID = -2147483648
MAX_FOOTNOTE_ID = 32767
MAX_RELATIONSHIP_ID_LENGTH = 255
FOOTNOTE_REF_STYLE_INDEX = 38
FOOTNOTE_TEXT_STYLE_INDEX = 29


# ============================================================================
# BASIC UTILITIES (from footnotes.py)
# ============================================================================

def find_footnote_references(doc):
    """Find all footnote references in the document."""
    footnote_refs = []
    for para_idx, para in enumerate(doc.paragraphs):
        for run_idx, run in enumerate(para.runs):
            # Check if this run has superscript formatting
            if run.font.superscript:
                # Check if it's likely a footnote reference
                if run.text.isdigit() or run.text in "¹²³⁴⁵⁶⁷⁸⁹⁰†‡§¶":
                    footnote_refs.append({
                        'paragraph_index': para_idx,
                        'run_index': run_idx,
                        'text': run.text,
                        'paragraph': para,
                        'run': run
                    })
    return footnote_refs


def get_format_symbols(format_type: str, count: int) -> list:
    """Generate format symbols for footnote numbering."""
    symbols = []
    
    if format_type == "1, 2, 3":
        symbols = [str(i) for i in range(1, count + 1)]
    elif format_type == "i, ii, iii":
        # Roman numerals
        roman_map = [(10, 'x'), (9, 'ix'), (5, 'v'), (4, 'iv'), (1, 'i')]
        for i in range(1, count + 1):
            result = ''
            num = i
            for value, numeral in roman_map:
                count_sym, num = divmod(num, value)
                result += numeral * count_sym
            symbols.append(result)
    elif format_type == "a, b, c":
        # Alphabetic
        for i in range(1, count + 1):
            if i <= 26:
                symbols.append(chr(96 + i))
            else:
                # For numbers > 26, use aa, ab, etc.
                first = (i - 1) // 26
                second = (i - 1) % 26 + 1
                symbols.append(chr(96 + first) + chr(96 + second))
    elif format_type == "*, †, ‡":
        # Special symbols
        special = ['*', '†', '‡', '§', '¶', '#']
        for i in range(1, count + 1):
            if i <= len(special):
                symbols.append(special[i - 1])
            else:
                # Repeat symbols with numbers
                symbols.append(special[(i - 1) % len(special)] + str((i - 1) // len(special) + 1))
    else:
        # Default to numeric
        symbols = [str(i) for i in range(1, count + 1)]
    
    return symbols


def customize_footnote_formatting(doc, footnote_refs, format_symbols, start_number, footnote_style):
    """Apply custom formatting to footnotes."""
    count = 0
    for i, ref in enumerate(footnote_refs):
        if i < len(format_symbols):
            # Update the footnote reference text
            ref['run'].text = format_symbols[i]
            ref['run'].font.superscript = True
            
            # Apply style if available
            if footnote_style:
                try:
                    ref['paragraph'].style = footnote_style
                except:
                    pass
            count += 1
    return count


# ============================================================================
# ROBUST IMPLEMENTATION (consolidated from footnotes_robust.py)
# ============================================================================

def _get_safe_footnote_id(footnotes_root) -> int:
    """Get a safe footnote ID avoiding conflicts and reserved values."""
    nsmap = {'w': W_NS}
    existing_footnotes = footnotes_root.xpath('//w:footnote', namespaces=nsmap)
    
    used_ids = set()
    for fn in existing_footnotes:
        fn_id = fn.get(f'{{{W_NS}}}id')
        if fn_id:
            try:
                used_ids.add(int(fn_id))
            except ValueError:
                pass
    
    # Start from 2 to avoid reserved IDs
    candidate_id = 2
    while candidate_id in used_ids or candidate_id in RESERVED_FOOTNOTE_IDS:
        candidate_id += 1
        if candidate_id > MAX_FOOTNOTE_ID:
            raise ValueError("No available footnote IDs")
    
    return candidate_id


def _ensure_content_types(content_types_xml: bytes) -> bytes:
    """Ensure content types with proper namespace handling."""
    ct_tree = etree.fromstring(content_types_xml)
    
    # Content Types uses default namespace - must use namespace-aware XPath
    nsmap = {'ct': CT_NS}
    
    # Check for existing override with proper namespace
    existing_overrides = ct_tree.xpath(
        "//ct:Override[@PartName='/word/footnotes.xml']",
        namespaces=nsmap
    )
    
    if existing_overrides:
        return content_types_xml  # Already exists
    
    # Add override with proper namespace
    override = etree.Element(f'{{{CT_NS}}}Override',
        PartName='/word/footnotes.xml',
        ContentType='application/vnd.openxmlformats-officedocument.wordprocessingml.footnotes+xml'
    )
    ct_tree.append(override)
    
    return etree.tostring(ct_tree, encoding='UTF-8', xml_declaration=True, standalone="yes")


def _ensure_document_rels(document_rels_xml: bytes) -> bytes:
    """Ensure document relationships with proper namespace handling."""
    rels_tree = etree.fromstring(document_rels_xml)
    
    # Relationships uses default namespace - must use namespace-aware XPath
    nsmap = {'r': REL_NS}
    
    # Check for existing footnotes relationship with proper namespace
    existing_footnote_rels = rels_tree.xpath(
        "//r:Relationship[contains(@Type, 'footnotes')]",
        namespaces=nsmap
    )
    
    if existing_footnote_rels:
        return document_rels_xml  # Already exists
    
    # Generate unique rId using namespace-aware XPath
    all_rels = rels_tree.xpath('//r:Relationship', namespaces=nsmap)
    existing_ids = {rel.get('Id') for rel in all_rels if rel.get('Id')}
    rid_num = 1
    while f'rId{rid_num}' in existing_ids:
        rid_num += 1
    
    # Validate ID length
    new_rid = f'rId{rid_num}'
    if len(new_rid) > MAX_RELATIONSHIP_ID_LENGTH:
        raise ValueError(f"Relationship ID too long: {new_rid}")
    
    # Create relationship with proper namespace
    rel = etree.Element(f'{{{REL_NS}}}Relationship',
        Id=new_rid,
        Type='http://schemas.openxmlformats.org/officeDocument/2006/relationships/footnotes',
        Target='footnotes.xml'
    )
    rels_tree.append(rel)
    
    return etree.tostring(rels_tree, encoding='UTF-8', xml_declaration=True, standalone="yes")


def _create_minimal_footnotes_xml() -> bytes:
    """Create minimal footnotes.xml with separators."""
    xml = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<w:footnotes xmlns:w="{W_NS}">
    <w:footnote w:type="separator" w:id="-1">
        <w:p>
            <w:pPr>
                <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
            </w:pPr>
            <w:r>
                <w:separator/>
            </w:r>
        </w:p>
    </w:footnote>
    <w:footnote w:type="continuationSeparator" w:id="0">
        <w:p>
            <w:pPr>
                <w:spacing w:after="0" w:line="240" w:lineRule="auto"/>
            </w:pPr>
            <w:r>
                <w:continuationSeparator/>
            </w:r>
        </w:p>
    </w:footnote>
</w:footnotes>'''
    return xml.encode('utf-8')


def _ensure_footnote_styles(styles_root):
    """Ensure both FootnoteReference and FootnoteText styles exist."""
    nsmap = {'w': W_NS}
    
    # Check for FootnoteReference style
    ref_style = styles_root.xpath('//w:style[@w:styleId="FootnoteReference"]', namespaces=nsmap)
    if not ref_style:
        # Create FootnoteReference character style
        style = etree.Element(f'{{{W_NS}}}style',
            attrib={
                f'{{{W_NS}}}type': 'character',
                f'{{{W_NS}}}styleId': 'FootnoteReference'
            }
        )
        name = etree.SubElement(style, f'{{{W_NS}}}name')
        name.set(f'{{{W_NS}}}val', 'footnote reference')
        
        base = etree.SubElement(style, f'{{{W_NS}}}basedOn')
        base.set(f'{{{W_NS}}}val', 'DefaultParagraphFont')
        
        rPr = etree.SubElement(style, f'{{{W_NS}}}rPr')
        vert_align = etree.SubElement(rPr, f'{{{W_NS}}}vertAlign')
        vert_align.set(f'{{{W_NS}}}val', 'superscript')
        
        styles_root.append(style)
    
    # Check for FootnoteText style
    text_style = styles_root.xpath('//w:style[@w:styleId="FootnoteText"]', namespaces=nsmap)
    if not text_style:
        # Create FootnoteText paragraph style
        style = etree.Element(f'{{{W_NS}}}style',
            attrib={
                f'{{{W_NS}}}type': 'paragraph',
                f'{{{W_NS}}}styleId': 'FootnoteText'
            }
        )
        name = etree.SubElement(style, f'{{{W_NS}}}name')
        name.set(f'{{{W_NS}}}val', 'footnote text')
        
        base = etree.SubElement(style, f'{{{W_NS}}}basedOn')
        base.set(f'{{{W_NS}}}val', 'Normal')
        
        pPr = etree.SubElement(style, f'{{{W_NS}}}pPr')
        sz = etree.SubElement(pPr, f'{{{W_NS}}}sz')
        sz.set(f'{{{W_NS}}}val', '20')  # 10pt
        
        styles_root.append(style)


def add_footnote_robust(
    filename: str,
    search_text: Optional[str] = None,
    paragraph_index: Optional[int] = None,
    footnote_text: str = "",
    output_filename: Optional[str] = None,
    position: str = "after",
    validate_location: bool = True,
    auto_repair: bool = False
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
    """
    Add a footnote with robust validation and error handling.
    
    This is the main production-ready function with all fixes applied.
    """
    
    # Validate inputs
    if not search_text and paragraph_index is None:
        return False, "Must provide either search_text or paragraph_index", None
    
    if search_text and paragraph_index is not None:
        return False, "Cannot provide both search_text and paragraph_index", None
    
    if not os.path.exists(filename):
        return False, f"File not found: {filename}", None
    
    # Set working file
    working_file = output_filename if output_filename else filename
    if output_filename and filename != output_filename:
        import shutil
        shutil.copy2(filename, output_filename)
    
    try:
        # Read document parts
        doc_parts = {}
        with zipfile.ZipFile(filename, 'r') as zin:
            doc_parts['document'] = zin.read('word/document.xml')
            doc_parts['content_types'] = zin.read('[Content_Types].xml')
            doc_parts['document_rels'] = zin.read('word/_rels/document.xml.rels')
            
            # Read or create footnotes.xml
            if 'word/footnotes.xml' in zin.namelist():
                doc_parts['footnotes'] = zin.read('word/footnotes.xml')
            else:
                doc_parts['footnotes'] = _create_minimal_footnotes_xml()
            
            # Read styles
            if 'word/styles.xml' in zin.namelist():
                doc_parts['styles'] = zin.read('word/styles.xml')
            else:
                # Create minimal styles
                doc_parts['styles'] = b'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="http://schemas.openxmlformats.org/wordprocessingml/2006/main"/>'
        
        # Parse XML documents
        doc_root = etree.fromstring(doc_parts['document'])
        footnotes_root = etree.fromstring(doc_parts['footnotes'])
        styles_root = etree.fromstring(doc_parts['styles'])
        
        # Find target location
        nsmap = {'w': W_NS}
        
        if search_text:
            # Search for text in paragraphs
            found = False
            for para in doc_root.xpath('//w:p', namespaces=nsmap):
                para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap))
                if search_text in para_text:
                    target_para = para
                    found = True
                    break
            
            if not found:
                return False, f"Text '{search_text}' not found in document", None
        else:
            # Use paragraph index
            paragraphs = doc_root.xpath('//w:p', namespaces=nsmap)
            if paragraph_index >= len(paragraphs):
                return False, f"Paragraph index {paragraph_index} out of range", None
            target_para = paragraphs[paragraph_index]
        
        # Validate location if requested
        if validate_location:
            # Check if paragraph is in header/footer
            parent = target_para.getparent()
            while parent is not None:
                if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']:
                    return False, "Cannot add footnote in header/footer", None
                parent = parent.getparent()
        
        # Get safe footnote ID
        footnote_id = _get_safe_footnote_id(footnotes_root)
        
        # Add footnote reference to document
        if position == "after":
            # Find last run in paragraph or create one
            runs = target_para.xpath('.//w:r', namespaces=nsmap)
            if runs:
                last_run = runs[-1]
                # Insert after last run
                insert_pos = target_para.index(last_run) + 1
            else:
                insert_pos = len(target_para)
        else:  # before
            # Find first run with text
            runs = target_para.xpath('.//w:r[w:t]', namespaces=nsmap)
            if runs:
                first_run = runs[0]
                insert_pos = target_para.index(first_run)
            else:
                insert_pos = 0
        
        # Create footnote reference run
        ref_run = etree.Element(f'{{{W_NS}}}r')
        
        # Add run properties with superscript
        rPr = etree.SubElement(ref_run, f'{{{W_NS}}}rPr')
        rStyle = etree.SubElement(rPr, f'{{{W_NS}}}rStyle')
        rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference')
        
        # Add footnote reference
        fn_ref = etree.SubElement(ref_run, f'{{{W_NS}}}footnoteReference')
        fn_ref.set(f'{{{W_NS}}}id', str(footnote_id))
        
        # Insert the reference run
        target_para.insert(insert_pos, ref_run)
        
        # Add footnote content
        new_footnote = etree.Element(f'{{{W_NS}}}footnote',
            attrib={f'{{{W_NS}}}id': str(footnote_id)}
        )
        
        # Add paragraph to footnote
        fn_para = etree.SubElement(new_footnote, f'{{{W_NS}}}p')
        
        # Add paragraph properties
        pPr = etree.SubElement(fn_para, f'{{{W_NS}}}pPr')
        pStyle = etree.SubElement(pPr, f'{{{W_NS}}}pStyle')
        pStyle.set(f'{{{W_NS}}}val', 'FootnoteText')
        
        # Add the footnote reference marker
        marker_run = etree.SubElement(fn_para, f'{{{W_NS}}}r')
        marker_rPr = etree.SubElement(marker_run, f'{{{W_NS}}}rPr')
        marker_rStyle = etree.SubElement(marker_rPr, f'{{{W_NS}}}rStyle')
        marker_rStyle.set(f'{{{W_NS}}}val', 'FootnoteReference')
        marker_ref = etree.SubElement(marker_run, f'{{{W_NS}}}footnoteRef')
        
        # Add space after marker
        space_run = etree.SubElement(fn_para, f'{{{W_NS}}}r')
        space_text = etree.SubElement(space_run, f'{{{W_NS}}}t')
        space_text.set(f'{{{XML_NS}}}space', 'preserve')
        space_text.text = ' '
        
        # Add footnote text
        text_run = etree.SubElement(fn_para, f'{{{W_NS}}}r')
        text_elem = etree.SubElement(text_run, f'{{{W_NS}}}t')
        text_elem.text = footnote_text
        
        # Append footnote to footnotes.xml
        footnotes_root.append(new_footnote)
        
        # Ensure styles exist
        _ensure_footnote_styles(styles_root)
        
        # Ensure coherence
        content_types_xml = _ensure_content_types(doc_parts['content_types'])
        document_rels_xml = _ensure_document_rels(doc_parts['document_rels'])
        
        # Write modified document
        temp_file = working_file + '.tmp'
        with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout:
            with zipfile.ZipFile(filename, 'r') as zin:
                # Copy unchanged files
                for item in zin.infolist():
                    if item.filename not in [
                        'word/document.xml', 'word/footnotes.xml', 'word/styles.xml',
                        '[Content_Types].xml', 'word/_rels/document.xml.rels'
                    ]:
                        zout.writestr(item, zin.read(item.filename))
            
            # Write modified files
            zout.writestr('word/document.xml',
                etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
            zout.writestr('word/footnotes.xml',
                etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
            zout.writestr('word/styles.xml',
                etree.tostring(styles_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
            zout.writestr('[Content_Types].xml', content_types_xml)
            zout.writestr('word/_rels/document.xml.rels', document_rels_xml)
        
        # Replace original with temp file
        os.replace(temp_file, working_file)
        
        details = {
            'footnote_id': footnote_id,
            'location': 'search_text' if search_text else 'paragraph_index',
            'styles_created': ['FootnoteReference', 'FootnoteText'],
            'coherence_verified': True
        }
        
        return True, f"Successfully added footnote (ID: {footnote_id}) to {working_file}", details
        
    except Exception as e:
        # Clean up temp file if exists
        temp_file = working_file + '.tmp'
        if os.path.exists(temp_file):
            os.remove(temp_file)
        return False, f"Error adding footnote: {str(e)}", None


def delete_footnote_robust(
    filename: str,
    footnote_id: Optional[int] = None,
    search_text: Optional[str] = None,
    output_filename: Optional[str] = None,
    clean_orphans: bool = True
) -> Tuple[bool, str, Optional[Dict[str, Any]]]:
    """Delete a footnote with comprehensive cleanup."""
    
    if not footnote_id and not search_text:
        return False, "Must provide either footnote_id or search_text", None
    
    if not os.path.exists(filename):
        return False, f"File not found: {filename}", None
    
    # Set working file
    working_file = output_filename if output_filename else filename
    if output_filename and filename != output_filename:
        import shutil
        shutil.copy2(filename, output_filename)
    
    try:
        # Read document parts
        with zipfile.ZipFile(filename, 'r') as zin:
            doc_xml = zin.read('word/document.xml')
            
            if 'word/footnotes.xml' not in zin.namelist():
                return False, "No footnotes in document", None
            
            footnotes_xml = zin.read('word/footnotes.xml')
        
        # Parse documents
        doc_root = etree.fromstring(doc_xml)
        footnotes_root = etree.fromstring(footnotes_xml)
        nsmap = {'w': W_NS}
        
        # Find footnote to delete
        if search_text:
            # Find footnote reference near text
            for para in doc_root.xpath('//w:p', namespaces=nsmap):
                para_text = ''.join(para.xpath('.//w:t/text()', namespaces=nsmap))
                if search_text in para_text:
                    # Look for footnote reference in this paragraph
                    fn_refs = para.xpath('.//w:footnoteReference', namespaces=nsmap)
                    if fn_refs:
                        footnote_id = int(fn_refs[0].get(f'{{{W_NS}}}id'))
                        break
            
            if not footnote_id:
                return False, f"No footnote found near text '{search_text}'", None
        
        # Remove footnote reference from document
        refs_removed = 0
        for fn_ref in doc_root.xpath(f'//w:footnoteReference[@w:id="{footnote_id}"]', namespaces=nsmap):
            # Remove the entire run containing the reference
            run = fn_ref.getparent()
            if run is not None and run.tag == f'{{{W_NS}}}r':
                para = run.getparent()
                if para is not None:
                    para.remove(run)
                    refs_removed += 1
        
        if refs_removed == 0:
            return False, f"Footnote {footnote_id} not found", None
        
        # Remove footnote content
        content_removed = 0
        for fn in footnotes_root.xpath(f'//w:footnote[@w:id="{footnote_id}"]', namespaces=nsmap):
            footnotes_root.remove(fn)
            content_removed += 1
        
        # Clean orphans if requested
        orphans_removed = []
        if clean_orphans:
            # Find all referenced IDs
            referenced_ids = set()
            for ref in doc_root.xpath('//w:footnoteReference', namespaces=nsmap):
                ref_id = ref.get(f'{{{W_NS}}}id')
                if ref_id:
                    referenced_ids.add(ref_id)
            
            # Remove unreferenced footnotes (except separators)
            for fn in footnotes_root.xpath('//w:footnote', namespaces=nsmap):
                fn_id = fn.get(f'{{{W_NS}}}id')
                if fn_id and fn_id not in referenced_ids and fn_id not in ['-1', '0']:
                    footnotes_root.remove(fn)
                    orphans_removed.append(fn_id)
        
        # Write modified document
        temp_file = working_file + '.tmp'
        with zipfile.ZipFile(temp_file, 'w', zipfile.ZIP_DEFLATED) as zout:
            with zipfile.ZipFile(filename, 'r') as zin:
                for item in zin.infolist():
                    if item.filename == 'word/document.xml':
                        zout.writestr(item,
                            etree.tostring(doc_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
                    elif item.filename == 'word/footnotes.xml':
                        zout.writestr(item,
                            etree.tostring(footnotes_root, encoding='UTF-8', xml_declaration=True, standalone="yes"))
                    else:
                        zout.writestr(item, zin.read(item.filename))
        
        os.replace(temp_file, working_file)
        
        details = {
            'footnote_id': footnote_id,
            'references_removed': refs_removed,
            'content_removed': content_removed,
            'orphans_removed': orphans_removed
        }
        
        message = f"Successfully deleted footnote {footnote_id}"
        if orphans_removed:
            message += f" and {len(orphans_removed)} orphaned footnotes"
        
        return True, message, details
        
    except Exception as e:
        return False, f"Error deleting footnote: {str(e)}", None


def validate_document_footnotes(filename: str) -> Tuple[bool, str, Dict[str, Any]]:
    """Validate all footnotes in a document for coherence and compliance."""
    
    if not os.path.exists(filename):
        return False, f"File not found: {filename}", {}
    
    report = {
        'total_references': 0,
        'total_content': 0,
        'id_conflicts': [],
        'orphaned_content': [],
        'missing_references': [],
        'invalid_locations': [],
        'missing_styles': [],
        'coherence_issues': []
    }
    
    try:
        with zipfile.ZipFile(filename, 'r') as zf:
            # Check document.xml
            doc_xml = zf.read('word/document.xml')
            doc_root = etree.fromstring(doc_xml)
            nsmap = {'w': W_NS}
            
            # Get all footnote references
            ref_ids = set()
            for ref in doc_root.xpath('//w:footnoteReference', namespaces=nsmap):
                ref_id = ref.get(f'{{{W_NS}}}id')
                if ref_id:
                    ref_ids.add(ref_id)
                    report['total_references'] += 1
                    
                    # Check location
                    parent = ref.getparent()
                    while parent is not None:
                        if parent.tag in [f'{{{W_NS}}}hdr', f'{{{W_NS}}}ftr']:
                            report['invalid_locations'].append(ref_id)
                            break
                        parent = parent.getparent()
            
            # Check footnotes.xml
            if 'word/footnotes.xml' in zf.namelist():
                footnotes_xml = zf.read('word/footnotes.xml')
                footnotes_root = etree.fromstring(footnotes_xml)
                
                content_ids = set()
                for fn in footnotes_root.xpath('//w:footnote', namespaces=nsmap):
                    fn_id = fn.get(f'{{{W_NS}}}id')
                    if fn_id:
                        content_ids.add(fn_id)
                        if fn_id not in ['-1', '0']:  # Exclude separators
                            report['total_content'] += 1
                
                # Find orphans and missing
                report['orphaned_content'] = list(content_ids - ref_ids - {'-1', '0'})
                report['missing_references'] = list(ref_ids - content_ids)
            else:
                if report['total_references'] > 0:
                    report['coherence_issues'].append('References exist but no footnotes.xml')
            
            # Check relationships
            if 'word/_rels/document.xml.rels' in zf.namelist():
                rels_xml = zf.read('word/_rels/document.xml.rels')
                rels_root = etree.fromstring(rels_xml)
                rel_nsmap = {'r': REL_NS}
                
                fn_rels = rels_root.xpath(
                    "//r:Relationship[contains(@Type, 'footnotes')]",
                    namespaces=rel_nsmap
                )
                
                if report['total_content'] > 0 and len(fn_rels) == 0:
                    report['coherence_issues'].append('Missing footnotes relationship')
                elif len(fn_rels) > 1:
                    report['coherence_issues'].append(f'Multiple footnote relationships: {len(fn_rels)}')
            
            # Check content types
            if '[Content_Types].xml' in zf.namelist():
                ct_xml = zf.read('[Content_Types].xml')
                ct_root = etree.fromstring(ct_xml)
                ct_nsmap = {'ct': CT_NS}
                
                fn_overrides = ct_root.xpath(
                    "//ct:Override[@PartName='/word/footnotes.xml']",
                    namespaces=ct_nsmap
                )
                
                if report['total_content'] > 0 and len(fn_overrides) == 0:
                    report['coherence_issues'].append('Missing footnotes content type')
                elif len(fn_overrides) > 1:
                    report['coherence_issues'].append(f'Multiple footnote content types: {len(fn_overrides)}')
            
            # Check styles
            if 'word/styles.xml' in zf.namelist():
                styles_xml = zf.read('word/styles.xml')
                styles_root = etree.fromstring(styles_xml)
                
                ref_style = styles_root.xpath('//w:style[@w:styleId="FootnoteReference"]', namespaces=nsmap)
                text_style = styles_root.xpath('//w:style[@w:styleId="FootnoteText"]', namespaces=nsmap)
                
                if not ref_style:
                    report['missing_styles'].append('FootnoteReference')
                if not text_style:
                    report['missing_styles'].append('FootnoteText')
        
        # Determine if valid
        is_valid = (
            len(report['id_conflicts']) == 0 and
            len(report['orphaned_content']) == 0 and
            len(report['missing_references']) == 0 and
            len(report['invalid_locations']) == 0 and
            len(report['coherence_issues']) == 0
        )
        
        if is_valid:
            message = "Document footnotes are valid"
        else:
            message = "Document has footnote issues"
        
        return is_valid, message, report
        
    except Exception as e:
        return False, f"Error validating document: {str(e)}", report


# ============================================================================
# COMPATIBILITY FUNCTIONS (for backward compatibility)
# ============================================================================

def add_footnote_at_paragraph_end(
    filename: str,
    paragraph_index: int,
    footnote_text: str,
    output_filename: Optional[str] = None
) -> Tuple[bool, str]:
    """Add footnote at the end of a specific paragraph (backward compatibility)."""
    success, message, _ = add_footnote_robust(
        filename=filename,
        paragraph_index=paragraph_index,
        footnote_text=footnote_text,
        output_filename=output_filename,
        position="after"
    )
    return success, message


def add_footnote_with_proper_formatting(
    filename: str,
    search_text: str,
    footnote_text: str,
    output_filename: Optional[str] = None,
    position: str = "after"
) -> Tuple[bool, str]:
    """Add footnote with proper formatting (backward compatibility)."""
    success, message, _ = add_footnote_robust(
        filename=filename,
        search_text=search_text,
        footnote_text=footnote_text,
        output_filename=output_filename,
        position=position
    )
    return success, message


def delete_footnote(
    filename: str,
    footnote_id: Optional[int] = None,
    search_text: Optional[str] = None,
    output_filename: Optional[str] = None
) -> Tuple[bool, str]:
    """Delete a footnote (backward compatibility)."""
    success, message, _ = delete_footnote_robust(
        filename=filename,
        footnote_id=footnote_id,
        search_text=search_text,
        output_filename=output_filename
    )
    return success, message


# ============================================================================
# LEGACY FUNCTIONS (for core/__init__.py compatibility)
# ============================================================================

def add_footnote(doc, paragraph_index: int, footnote_text: str):
    """Legacy function for adding footnotes to python-docx Document objects.
    Note: This is a simplified version that doesn't create proper Word footnotes."""
    if paragraph_index >= len(doc.paragraphs):
        raise IndexError(f"Paragraph index {paragraph_index} out of range")
    
    para = doc.paragraphs[paragraph_index]
    # Add superscript number
    run = para.add_run()
    run.text = "¹"
    run.font.superscript = True
    
    # Add footnote text at document end
    doc.add_paragraph("_" * 50)
    footnote_para = doc.add_paragraph(f"¹ {footnote_text}")
    footnote_para.style = "Caption"
    
    return doc


def add_endnote(doc, paragraph_index: int, endnote_text: str):
    """Legacy function for adding endnotes."""
    if paragraph_index >= len(doc.paragraphs):
        raise IndexError(f"Paragraph index {paragraph_index} out of range")
    
    para = doc.paragraphs[paragraph_index]
    run = para.add_run()
    run.text = "†"
    run.font.superscript = True
    
    # Endnotes go at the very end
    doc.add_page_break()
    doc.add_heading("Endnotes", level=1)
    endnote_para = doc.add_paragraph(f"† {endnote_text}")
    
    return doc


def convert_footnotes_to_endnotes(doc):
    """Legacy function to convert footnotes to endnotes in a Document object."""
    # This is a placeholder - real conversion requires XML manipulation
    return doc


# Define XML_NS if needed
XML_NS = 'http://www.w3.org/XML/1998/namespace'
```

--------------------------------------------------------------------------------
/word_document_server/tools/format_tools.py:
--------------------------------------------------------------------------------

```python
"""
Formatting tools for Word Document Server.

These tools handle formatting operations for Word documents,
including text formatting, table formatting, and custom styles.
"""
import os
from typing import List, Optional, Dict, Any
from docx import Document
from docx.shared import Pt, RGBColor
from docx.enum.text import WD_COLOR_INDEX
from docx.enum.style import WD_STYLE_TYPE

from word_document_server.utils.file_utils import check_file_writeable, ensure_docx_extension
from word_document_server.core.styles import create_style
from word_document_server.core.tables import (
    apply_table_style, set_cell_shading_by_position, apply_alternating_row_shading,
    highlight_header_row, merge_cells, merge_cells_horizontal, merge_cells_vertical,
    set_cell_alignment_by_position, set_table_alignment, set_column_width_by_position,
    set_column_widths, set_table_width as set_table_width_func, auto_fit_table,
    format_cell_text_by_position, set_cell_padding_by_position
)


async def format_text(filename: str, paragraph_index: int, start_pos: int, end_pos: int, 
                     bold: Optional[bool] = None, italic: Optional[bool] = None, 
                     underline: Optional[bool] = None, color: Optional[str] = None,
                     font_size: Optional[int] = None, font_name: Optional[str] = None) -> str:
    """Format a specific range of text within a paragraph.
    
    Args:
        filename: Path to the Word document
        paragraph_index: Index of the paragraph (0-based)
        start_pos: Start position within the paragraph text
        end_pos: End position within the paragraph text
        bold: Set text bold (True/False)
        italic: Set text italic (True/False)
        underline: Set text underlined (True/False)
        color: Text color (e.g., 'red', 'blue', etc.)
        font_size: Font size in points
        font_name: Font name/family
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        paragraph_index = int(paragraph_index)
        start_pos = int(start_pos)
        end_pos = int(end_pos)
        if font_size is not None:
            font_size = int(font_size)
    except (ValueError, TypeError):
        return "Invalid parameter: paragraph_index, start_pos, end_pos, and font_size must be integers"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate paragraph index
        if paragraph_index < 0 or paragraph_index >= len(doc.paragraphs):
            return f"Invalid paragraph index. Document has {len(doc.paragraphs)} paragraphs (0-{len(doc.paragraphs)-1})."
        
        paragraph = doc.paragraphs[paragraph_index]
        text = paragraph.text
        
        # Validate text positions
        if start_pos < 0 or end_pos > len(text) or start_pos >= end_pos:
            return f"Invalid text positions. Paragraph has {len(text)} characters."
        
        # Get the text to format
        target_text = text[start_pos:end_pos]
        
        # Clear existing runs and create three runs: before, target, after
        for run in paragraph.runs:
            run.clear()
        
        # Add text before target
        if start_pos > 0:
            run_before = paragraph.add_run(text[:start_pos])
        
        # Add target text with formatting
        run_target = paragraph.add_run(target_text)
        if bold is not None:
            run_target.bold = bold
        if italic is not None:
            run_target.italic = italic
        if underline is not None:
            run_target.underline = underline
        if color:
            # Define common RGB colors
            color_map = {
                'red': RGBColor(255, 0, 0),
                'blue': RGBColor(0, 0, 255),
                'green': RGBColor(0, 128, 0),
                'yellow': RGBColor(255, 255, 0),
                'black': RGBColor(0, 0, 0),
                'gray': RGBColor(128, 128, 128),
                'white': RGBColor(255, 255, 255),
                'purple': RGBColor(128, 0, 128),
                'orange': RGBColor(255, 165, 0)
            }
            
            try:
                if color.lower() in color_map:
                    # Use predefined RGB color
                    run_target.font.color.rgb = color_map[color.lower()]
                else:
                    # Try to set color by name
                    run_target.font.color.rgb = RGBColor.from_string(color)
            except Exception as e:
                # If all else fails, default to black
                run_target.font.color.rgb = RGBColor(0, 0, 0)
        if font_size:
            run_target.font.size = Pt(font_size)
        if font_name:
            run_target.font.name = font_name
        
        # Add text after target
        if end_pos < len(text):
            run_after = paragraph.add_run(text[end_pos:])
        
        doc.save(filename)
        return f"Text '{target_text}' formatted successfully in paragraph {paragraph_index}."
    except Exception as e:
        return f"Failed to format text: {str(e)}"


async def create_custom_style(filename: str, style_name: str, 
                             bold: Optional[bool] = None, italic: Optional[bool] = None,
                             font_size: Optional[int] = None, font_name: Optional[str] = None,
                             color: Optional[str] = None, base_style: Optional[str] = None) -> str:
    """Create a custom style in the document.
    
    Args:
        filename: Path to the Word document
        style_name: Name for the new style
        bold: Set text bold (True/False)
        italic: Set text italic (True/False)
        font_size: Font size in points
        font_name: Font name/family
        color: Text color (e.g., 'red', 'blue')
        base_style: Optional existing style to base this on
    """
    filename = ensure_docx_extension(filename)
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Build font properties dictionary
        font_properties = {}
        if bold is not None:
            font_properties['bold'] = bold
        if italic is not None:
            font_properties['italic'] = italic
        if font_size is not None:
            font_properties['size'] = font_size
        if font_name is not None:
            font_properties['name'] = font_name
        if color is not None:
            font_properties['color'] = color
        
        # Create the style
        new_style = create_style(
            doc, 
            style_name, 
            WD_STYLE_TYPE.PARAGRAPH, 
            base_style=base_style,
            font_properties=font_properties
        )
        
        doc.save(filename)
        return f"Style '{style_name}' created successfully."
    except Exception as e:
        return f"Failed to create style: {str(e)}"


async def format_table(filename: str, table_index: int, 
                      has_header_row: Optional[bool] = None,
                      border_style: Optional[str] = None,
                      shading: Optional[List[List[str]]] = None) -> str:
    """Format a table with borders, shading, and structure.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        has_header_row: If True, formats the first row as a header
        border_style: Style for borders ('none', 'single', 'double', 'thick')
        shading: 2D list of cell background colors (by row and column)
    """
    filename = ensure_docx_extension(filename)
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply formatting
        success = apply_table_style(table, has_header_row or False, border_style, shading)
        
        if success:
            doc.save(filename)
            return f"Table at index {table_index} formatted successfully."
        else:
            return f"Failed to format table at index {table_index}."
    except Exception as e:
        return f"Failed to format table: {str(e)}"


async def set_table_cell_shading(filename: str, table_index: int, row_index: int, 
                                col_index: int, fill_color: str, pattern: str = "clear") -> str:
    """Apply shading/filling to a specific table cell.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        row_index: Row index of the cell (0-based)
        col_index: Column index of the cell (0-based)
        fill_color: Background color (hex string like "FF0000" or "red")
        pattern: Shading pattern ("clear", "solid", "pct10", "pct20", etc.)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        row_index = int(row_index)
        col_index = int(col_index)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index, row_index, and col_index must be integers"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Validate row and column indices
        if row_index < 0 or row_index >= len(table.rows):
            return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})."
        
        if col_index < 0 or col_index >= len(table.rows[row_index].cells):
            return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})."
        
        # Apply cell shading
        success = set_cell_shading_by_position(table, row_index, col_index, fill_color, pattern)
        
        if success:
            doc.save(filename)
            return f"Cell shading applied successfully to table {table_index}, row {row_index}, column {col_index}."
        else:
            return f"Failed to apply cell shading."
    except Exception as e:
        return f"Failed to apply cell shading: {str(e)}"


async def apply_table_alternating_rows(filename: str, table_index: int, 
                                     color1: str = "FFFFFF", color2: str = "F2F2F2") -> str:
    """Apply alternating row colors to a table for better readability.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        color1: Color for odd rows (hex string, default white)
        color2: Color for even rows (hex string, default light gray)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index must be an integer"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply alternating row shading
        success = apply_alternating_row_shading(table, color1, color2)
        
        if success:
            doc.save(filename)
            return f"Alternating row shading applied successfully to table {table_index}."
        else:
            return f"Failed to apply alternating row shading."
    except Exception as e:
        return f"Failed to apply alternating row shading: {str(e)}"


async def highlight_table_header(filename: str, table_index: int, 
                               header_color: str = "4472C4", text_color: str = "FFFFFF") -> str:
    """Apply special highlighting to table header row.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        header_color: Background color for header (hex string, default blue)
        text_color: Text color for header (hex string, default white)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index must be an integer"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply header highlighting
        success = highlight_header_row(table, header_color, text_color)
        
        if success:
            doc.save(filename)
            return f"Header highlighting applied successfully to table {table_index}."
        else:
            return f"Failed to apply header highlighting."
    except Exception as e:
        return f"Failed to apply header highlighting: {str(e)}"


async def merge_table_cells(filename: str, table_index: int, start_row: int, start_col: int, 
                          end_row: int, end_col: int) -> str:
    """Merge cells in a rectangular area of a table.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        start_row: Starting row index (0-based)
        start_col: Starting column index (0-based)
        end_row: Ending row index (0-based, inclusive)
        end_col: Ending column index (0-based, inclusive)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        start_row = int(start_row)
        start_col = int(start_col)
        end_row = int(end_row)
        end_col = int(end_col)
    except (ValueError, TypeError):
        return "Invalid parameter: all indices must be integers"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Validate merge parameters
        if start_row > end_row or start_col > end_col:
            return "Invalid merge range: start indices must be <= end indices"
        
        if start_row == end_row and start_col == end_col:
            return "Invalid merge range: cannot merge a single cell with itself"
        
        # Apply cell merge
        success = merge_cells(table, start_row, start_col, end_row, end_col)
        
        if success:
            doc.save(filename)
            return f"Cells merged successfully in table {table_index} from ({start_row},{start_col}) to ({end_row},{end_col})."
        else:
            return f"Failed to merge cells. Check that indices are valid."
    except Exception as e:
        return f"Failed to merge cells: {str(e)}"


async def merge_table_cells_horizontal(filename: str, table_index: int, row_index: int, 
                                     start_col: int, end_col: int) -> str:
    """Merge cells horizontally in a single row.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        row_index: Row index (0-based)
        start_col: Starting column index (0-based)
        end_col: Ending column index (0-based, inclusive)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        row_index = int(row_index)
        start_col = int(start_col)
        end_col = int(end_col)
    except (ValueError, TypeError):
        return "Invalid parameter: all indices must be integers"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply horizontal cell merge
        success = merge_cells_horizontal(table, row_index, start_col, end_col)
        
        if success:
            doc.save(filename)
            return f"Cells merged horizontally in table {table_index}, row {row_index}, columns {start_col}-{end_col}."
        else:
            return f"Failed to merge cells horizontally. Check that indices are valid."
    except Exception as e:
        return f"Failed to merge cells horizontally: {str(e)}"


async def merge_table_cells_vertical(filename: str, table_index: int, col_index: int, 
                                   start_row: int, end_row: int) -> str:
    """Merge cells vertically in a single column.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        col_index: Column index (0-based)
        start_row: Starting row index (0-based)
        end_row: Ending row index (0-based, inclusive)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        col_index = int(col_index)
        start_row = int(start_row)
        end_row = int(end_row)
    except (ValueError, TypeError):
        return "Invalid parameter: all indices must be integers"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply vertical cell merge
        success = merge_cells_vertical(table, col_index, start_row, end_row)
        
        if success:
            doc.save(filename)
            return f"Cells merged vertically in table {table_index}, column {col_index}, rows {start_row}-{end_row}."
        else:
            return f"Failed to merge cells vertically. Check that indices are valid."
    except Exception as e:
        return f"Failed to merge cells vertically: {str(e)}"


async def set_table_cell_alignment(filename: str, table_index: int, row_index: int, col_index: int,
                                 horizontal: str = "left", vertical: str = "top") -> str:
    """Set text alignment for a specific table cell.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        horizontal: Horizontal alignment ("left", "center", "right", "justify")
        vertical: Vertical alignment ("top", "center", "bottom")
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        row_index = int(row_index)
        col_index = int(col_index)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index, row_index, and col_index must be integers"
    
    # Validate alignment parameters
    valid_horizontal = ["left", "center", "right", "justify"]
    valid_vertical = ["top", "center", "bottom"]
    
    if horizontal.lower() not in valid_horizontal:
        return f"Invalid horizontal alignment. Valid options: {', '.join(valid_horizontal)}"
    
    if vertical.lower() not in valid_vertical:
        return f"Invalid vertical alignment. Valid options: {', '.join(valid_vertical)}"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply cell alignment
        success = set_cell_alignment_by_position(table, row_index, col_index, horizontal, vertical)
        
        if success:
            doc.save(filename)
            return f"Cell alignment set successfully for table {table_index}, cell ({row_index},{col_index}) to {horizontal}/{vertical}."
        else:
            return f"Failed to set cell alignment. Check that indices are valid."
    except Exception as e:
        return f"Failed to set cell alignment: {str(e)}"


async def set_table_alignment_all(filename: str, table_index: int, 
                                horizontal: str = "left", vertical: str = "top") -> str:
    """Set text alignment for all cells in a table.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        horizontal: Horizontal alignment ("left", "center", "right", "justify")
        vertical: Vertical alignment ("top", "center", "bottom")
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index must be an integer"
    
    # Validate alignment parameters
    valid_horizontal = ["left", "center", "right", "justify"]
    valid_vertical = ["top", "center", "bottom"]
    
    if horizontal.lower() not in valid_horizontal:
        return f"Invalid horizontal alignment. Valid options: {', '.join(valid_horizontal)}"
    
    if vertical.lower() not in valid_vertical:
        return f"Invalid vertical alignment. Valid options: {', '.join(valid_vertical)}"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply table alignment
        success = set_table_alignment(table, horizontal, vertical)
        
        if success:
            doc.save(filename)
            return f"Table alignment set successfully for table {table_index} to {horizontal}/{vertical} for all cells."
        else:
            return f"Failed to set table alignment."
    except Exception as e:
        return f"Failed to set table alignment: {str(e)}"


async def set_table_column_width(filename: str, table_index: int, col_index: int, 
                                width: float, width_type: str = "points") -> str:
    """Set the width of a specific table column.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        col_index: Column index (0-based)
        width: Column width value
        width_type: Width type ("points", "inches", "cm", "percent", "auto")
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        col_index = int(col_index)
        if width_type != "auto":
            width = float(width)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index and col_index must be integers, width must be a number"
    
    # Validate width type
    valid_width_types = ["points", "inches", "cm", "percent", "auto"]
    if width_type.lower() not in valid_width_types:
        return f"Invalid width type. Valid options: {', '.join(valid_width_types)}"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Validate column index
        if col_index < 0 or col_index >= len(table.columns):
            return f"Invalid column index. Table has {len(table.columns)} columns (0-{len(table.columns)-1})."
        
        # Convert width and type for Word format
        if width_type.lower() == "points":
            # Points to DXA (twentieths of a point)
            word_width = width
            word_type = "dxa"
        elif width_type.lower() == "inches":
            # Inches to points, then to DXA
            word_width = width * 72  # 72 points per inch
            word_type = "dxa"
        elif width_type.lower() == "cm":
            # CM to points, then to DXA
            word_width = width * 28.35  # ~28.35 points per cm
            word_type = "dxa"
        elif width_type.lower() == "percent":
            # Percentage (Word uses 50x the percentage value)
            word_width = width
            word_type = "pct"
        else:  # auto
            word_width = 0
            word_type = "auto"
        
        # Apply column width
        success = set_column_width_by_position(table, col_index, word_width, word_type)
        
        if success:
            doc.save(filename)
            return f"Column width set successfully for table {table_index}, column {col_index} to {width} {width_type}."
        else:
            return f"Failed to set column width. Check that indices are valid."
    except Exception as e:
        return f"Failed to set column width: {str(e)}"


async def set_table_column_widths(filename: str, table_index: int, widths: list, 
                                 width_type: str = "points") -> str:
    """Set the widths of multiple table columns.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        widths: List of width values for each column
        width_type: Width type ("points", "inches", "cm", "percent", "auto")
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        if width_type != "auto":
            widths = [float(w) for w in widths]
    except (ValueError, TypeError):
        return "Invalid parameter: table_index must be an integer, widths must be a list of numbers"
    
    # Validate width type
    valid_width_types = ["points", "inches", "cm", "percent", "auto"]
    if width_type.lower() not in valid_width_types:
        return f"Invalid width type. Valid options: {', '.join(valid_width_types)}"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Convert widths and type for Word format
        word_widths = []
        for width in widths:
            if width_type.lower() == "points":
                word_widths.append(width)
            elif width_type.lower() == "inches":
                word_widths.append(width * 72)  # 72 points per inch
            elif width_type.lower() == "cm":
                word_widths.append(width * 28.35)  # ~28.35 points per cm
            elif width_type.lower() == "percent":
                word_widths.append(width)
            else:  # auto
                word_widths.append(0)
        
        # Determine Word type
        if width_type.lower() == "percent":
            word_type = "pct"
        elif width_type.lower() == "auto":
            word_type = "auto"
        else:
            word_type = "dxa"
        
        # Apply column widths
        success = set_column_widths(table, word_widths, word_type)
        
        if success:
            doc.save(filename)
            return f"Column widths set successfully for table {table_index} with {len(widths)} columns in {width_type}."
        else:
            return f"Failed to set column widths."
    except Exception as e:
        return f"Failed to set column widths: {str(e)}"


async def set_table_width(filename: str, table_index: int, width: float, 
                         width_type: str = "points") -> str:
    """Set the overall width of a table.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        width: Table width value
        width_type: Width type ("points", "inches", "cm", "percent", "auto")
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        if width_type != "auto":
            width = float(width)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index must be an integer, width must be a number"
    
    # Validate width type
    valid_width_types = ["points", "inches", "cm", "percent", "auto"]
    if width_type.lower() not in valid_width_types:
        return f"Invalid width type. Valid options: {', '.join(valid_width_types)}"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Convert width and type for Word format
        if width_type.lower() == "points":
            word_width = width
            word_type = "dxa"
        elif width_type.lower() == "inches":
            word_width = width * 72  # 72 points per inch
            word_type = "dxa"
        elif width_type.lower() == "cm":
            word_width = width * 28.35  # ~28.35 points per cm
            word_type = "dxa"
        elif width_type.lower() == "percent":
            word_width = width
            word_type = "pct"
        else:  # auto
            word_width = 0
            word_type = "auto"
        
        # Apply table width
        success = set_table_width_func(table, word_width, word_type)
        
        if success:
            doc.save(filename)
            return f"Table width set successfully for table {table_index} to {width} {width_type}."
        else:
            return f"Failed to set table width."
    except Exception as e:
        return f"Failed to set table width: {str(e)}"


async def auto_fit_table_columns(filename: str, table_index: int) -> str:
    """Set table columns to auto-fit based on content.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index must be an integer"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Apply auto-fit
        success = auto_fit_table(table)
        
        if success:
            doc.save(filename)
            return f"Table {table_index} set to auto-fit columns based on content."
        else:
            return f"Failed to set table auto-fit."
    except Exception as e:
        return f"Failed to set table auto-fit: {str(e)}"


async def format_table_cell_text(filename: str, table_index: int, row_index: int, col_index: int,
                                 text_content: Optional[str] = None, bold: Optional[bool] = None, italic: Optional[bool] = None,
                                 underline: Optional[bool] = None, color: Optional[str] = None, font_size: Optional[int] = None,
                                 font_name: Optional[str] = None) -> str:
    """Format text within a specific table cell.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        text_content: Optional new text content for the cell
        bold: Set text bold (True/False)
        italic: Set text italic (True/False)
        underline: Set text underlined (True/False)
        color: Text color (hex string like "FF0000" or color name like "red")
        font_size: Font size in points
        font_name: Font name/family
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        row_index = int(row_index)
        col_index = int(col_index)
        if font_size is not None:
            font_size = int(font_size)
    except (ValueError, TypeError):
        return "Invalid parameter: table_index, row_index, col_index must be integers, font_size must be integer"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Validate row and column indices
        if row_index < 0 or row_index >= len(table.rows):
            return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})."
        
        if col_index < 0 or col_index >= len(table.rows[row_index].cells):
            return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})."
        
        # Apply cell text formatting
        success = format_cell_text_by_position(table, row_index, col_index, text_content, 
                                              bold, italic, underline, color, font_size, font_name)
        
        if success:
            doc.save(filename)
            format_desc = []
            if text_content is not None:
                format_desc.append(f"content='{text_content[:30]}{'...' if len(text_content) > 30 else ''}'")
            if bold is not None:
                format_desc.append(f"bold={bold}")
            if italic is not None:
                format_desc.append(f"italic={italic}")
            if underline is not None:
                format_desc.append(f"underline={underline}")
            if color is not None:
                format_desc.append(f"color={color}")
            if font_size is not None:
                format_desc.append(f"size={font_size}pt")
            if font_name is not None:
                format_desc.append(f"font={font_name}")
            
            format_str = ", ".join(format_desc) if format_desc else "no changes"
            return f"Cell text formatted successfully in table {table_index}, cell ({row_index},{col_index}): {format_str}."
        else:
            return f"Failed to format cell text. Check that indices are valid."
    except Exception as e:
        return f"Failed to format cell text: {str(e)}"


async def set_table_cell_padding(filename: str, table_index: int, row_index: int, col_index: int,
                                 top: Optional[float] = None, bottom: Optional[float] = None, left: Optional[float] = None, 
                                 right: Optional[float] = None, unit: str = "points") -> str:
    """Set padding/margins for a specific table cell.
    
    Args:
        filename: Path to the Word document
        table_index: Index of the table (0-based)
        row_index: Row index (0-based)
        col_index: Column index (0-based)
        top: Top padding in specified units
        bottom: Bottom padding in specified units
        left: Left padding in specified units
        right: Right padding in specified units
        unit: Unit type ("points" or "percent")
    """
    filename = ensure_docx_extension(filename)
    
    # Ensure numeric parameters are the correct type
    try:
        table_index = int(table_index)
        row_index = int(row_index)
        col_index = int(col_index)
        if top is not None:
            top = float(top)
        if bottom is not None:
            bottom = float(bottom)
        if left is not None:
            left = float(left)
        if right is not None:
            right = float(right)
    except (ValueError, TypeError):
        return "Invalid parameter: indices must be integers, padding values must be numbers"
    
    # Validate unit
    valid_units = ["points", "percent"]
    if unit.lower() not in valid_units:
        return f"Invalid unit. Valid options: {', '.join(valid_units)}"
    
    if not os.path.exists(filename):
        return f"Document {filename} does not exist"
    
    # Check if file is writeable
    is_writeable, error_message = check_file_writeable(filename)
    if not is_writeable:
        return f"Cannot modify document: {error_message}. Consider creating a copy first."
    
    try:
        doc = Document(filename)
        
        # Validate table index
        if table_index < 0 or table_index >= len(doc.tables):
            return f"Invalid table index. Document has {len(doc.tables)} tables (0-{len(doc.tables)-1})."
        
        table = doc.tables[table_index]
        
        # Validate row and column indices
        if row_index < 0 or row_index >= len(table.rows):
            return f"Invalid row index. Table has {len(table.rows)} rows (0-{len(table.rows)-1})."
        
        if col_index < 0 or col_index >= len(table.rows[row_index].cells):
            return f"Invalid column index. Row has {len(table.rows[row_index].cells)} cells (0-{len(table.rows[row_index].cells)-1})."
        
        # Convert unit for Word format
        word_unit = "dxa" if unit.lower() == "points" else "pct"
        
        # Apply cell padding
        success = set_cell_padding_by_position(table, row_index, col_index, top, bottom, 
                                              left, right, word_unit)
        
        if success:
            doc.save(filename)
            padding_desc = []
            if top is not None:
                padding_desc.append(f"top={top}")
            if bottom is not None:
                padding_desc.append(f"bottom={bottom}")
            if left is not None:
                padding_desc.append(f"left={left}")
            if right is not None:
                padding_desc.append(f"right={right}")
            
            padding_str = ", ".join(padding_desc) if padding_desc else "no padding"
            return f"Cell padding set successfully for table {table_index}, cell ({row_index},{col_index}): {padding_str} {unit}."
        else:
            return f"Failed to set cell padding. Check that indices are valid."
    except Exception as e:
        return f"Failed to set cell padding: {str(e)}"

```
Page 2/2FirstPrevNextLast