Files
md_to_gost/font.py
T
Igor20264 516abe7b83
Python application / build (push) Has been cancelled
BigUpdate
2026-09-03 10:44:08 +03:00

860 lines
29 KiB
Python

import docx.text.font as Font
from typing import Optional, Union, Dict, Any
from enum import Enum
class ThemeFont(Enum):
MAJOR_ASCII = "majorAscii"
MAJOR_H_ANSI = "majorHAnsi"
MAJOR_EAST_ASIAN = "majorEastAsia"
MAJOR_BIDI = "majorBidi"
MINOR_ASCII = "minorAscii"
MINOR_H_ANSI = "minorHAnsi"
MINOR_EAST_ASIAN = "minorEastAsia"
MINOR_BIDI = "minorBidi"
class Font:
"""
Класс, представляющий форматирование шрифта для пробега текста (run) в Word.
Поддерживает три-state значения (True/False/None) и множественные шрифты.
"""
def __init__(self):
# Основные шрифты
self._ascii: Optional[str] = None
self._h_ansi: Optional[str] = None
self._east_asia: Optional[str] = None
self._cs: Optional[str] = None
# Тематические шрифты (меньший приоритет)
self._ascii_theme: Optional[ThemeFont] = None
self._h_ansi_theme: Optional[ThemeFont] = None
self._east_asia_theme: Optional[ThemeFont] = None
self._cs_theme: Optional[ThemeFont] = None
# Три-state булевы свойства (None = унаследовано)
self._bold: Optional[bool] = None
self._italic: Optional[bool] = None
self._small_caps: Optional[bool] = None
self._strike: Optional[bool] = None
self._double_strike: Optional[bool] = None
self._outline: Optional[bool] = None
self._shadow: Optional[bool] = None
self._emboss: Optional[bool] = None
self._imprint: Optional[bool] = None
self._hidden: Optional[bool] = None
self._all_caps: Optional[bool] = None
self._complex_script: Optional[bool] = None
self._rtl: Optional[bool] = None
self._no_proof: Optional[bool] = None
# --- Свойство: name (основное имя шрифта) ---
@property
def name(self) -> Optional[str]:
"""Возвращает имя основного шрифта (w:ascii)."""
return self._ascii
@name.setter
def name(self, value: Optional[str]) -> None:
"""
Устанавливает w:ascii и w:hAnsi одновременно.
Если значение None — оба атрибута удаляются.
"""
if value is None:
self._ascii = None
self._h_ansi = None
else:
self._ascii = str(value)
self._h_ansi = str(value)
# --- Другие шрифты ---
@property
def east_asia(self) -> Optional[str]:
return self._east_asia
@east_asia.setter
def east_asia(self, value: Optional[str]) -> None:
self._east_asia = value
@property
def complex_script(self) -> Optional[str]:
return self._cs
@complex_script.setter
def complex_script(self, value: Optional[str]) -> None:
self._cs = value
@property
def ascii_theme(self) -> Optional[ThemeFont]:
return self._ascii_theme
@ascii_theme.setter
def ascii_theme(self, theme_font: Optional[ThemeFont]) -> None:
if isinstance(theme_font, ThemeFont) or theme_font is None:
self._ascii_theme = theme_font
else:
raise ValueError("ascii_theme must be a ThemeFont or None")
@property
def h_ansi_theme(self) -> Optional[ThemeFont]:
return self._h_ansi_theme
@h_ansi_theme.setter
def h_ansi_theme(self, theme_font: Optional[ThemeFont]) -> None:
self._h_ansi_theme = theme_font
# --- Три-state Boolean Properties (Toggle properties marked) ---
@property
def bold(self) -> Optional[bool]:
return self._bold
@bold.setter
def bold(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._bold = value
else:
raise ValueError("bold must be True, False, or None")
@property
def italic(self) -> Optional[bool]:
return self._italic
@italic.setter
def italic(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._italic = value
else:
raise ValueError("italic must be True, False, or None")
@property
def small_caps(self) -> Optional[bool]:
return self._small_caps
@small_caps.setter
def small_caps(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._small_caps = value
else:
raise ValueError("small_caps must be True, False, or None")
@property
def strike(self) -> Optional[bool]:
return self._strike
@strike.setter
def strike(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._strike = value
else:
raise ValueError("strike must be True, False, or None")
@property
def double_strike(self) -> Optional[bool]:
return self._double_strike
@double_strike.setter
def double_strike(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._double_strike = value
else:
raise ValueError("double_strike must be True, False, or None")
@property
def outline(self) -> Optional[bool]:
return self._outline
@outline.setter
def outline(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._outline = value
else:
raise ValueError("outline must be True, False, or None")
@property
def shadow(self) -> Optional[bool]:
return self._shadow
@shadow.setter
def shadow(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._shadow = value
else:
raise ValueError("shadow must be True, False, or None")
@property
def emboss(self) -> Optional[bool]:
return self._emboss
@emboss.setter
def emboss(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._emboss = value
else:
raise ValueError("emboss must be True, False, or None")
@property
def imprint(self) -> Optional[bool]:
return self._imprint
@imprint.setter
def imprint(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._imprint = value
else:
raise ValueError("imprint must be True, False, or None")
@property
def hidden(self) -> Optional[bool]:
return self._hidden
@hidden.setter
def hidden(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._hidden = value
else:
raise ValueError("hidden must be True, False, or None")
@property
def all_caps(self) -> Optional[bool]:
return self._all_caps
@all_caps.setter
def all_caps(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._all_caps = value
else:
raise ValueError("all_caps must be True, False, or None")
@property
def cs(self) -> Optional[bool]:
return self._complex_script
@cs.setter
def cs(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._complex_script = value
else:
raise ValueError("cs must be True, False, or None")
@property
def rtl(self) -> Optional[bool]:
return self._rtl
@rtl.setter
def rtl(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._rtl = value
else:
raise ValueError("rtl must be True, False, or None")
@property
def no_proof(self) -> Optional[bool]:
return self._no_proof
@no_proof.setter
def no_proof(self, value: Optional[bool]) -> None:
if value in (True, False, None):
self._no_proof = value
else:
raise ValueError("no_proof must be True, False, or None")
# --- Методы сериализации в XML / словарь ---
def to_xml_dict(self) -> Dict[str, Any]:
"""Преобразует текущее состояние шрифта в словарь, имитирующий XML."""
r_pr = {}
fonts = {}
if self._ascii:
fonts['ascii'] = self._ascii
if self._h_ansi:
fonts['hAnsi'] = self._h_ansi
if self._east_asia:
fonts['eastAsia'] = self._east_asia
if self._cs:
fonts['cs'] = self._cs
if self._ascii_theme:
fonts['asciiTheme'] = self._ascii_theme.value
if self._h_ansi_theme:
fonts['hAnsiTheme'] = self._h_ansi_theme.value
if self._east_asia_theme:
fonts['eastAsiaTheme'] = self._east_asia_theme.value
if self._cs_theme:
fonts['csTheme'] = self._cs_theme.value
if fonts:
r_pr['rFonts'] = fonts
# Boolean properties (если не None)
bool_map = {
'b': self._bold,
'i': self._italic,
'caps': self._all_caps,
'smallCaps': self._small_caps,
'strike': self._strike,
'dstrike': self._double_strike,
'outline': self._outline,
'shadow': self._shadow,
'emboss': self._emboss,
'imprint': self._imprint,
'vanish': self._hidden,
'cs': self._complex_script,
'rtl': self._rtl,
'noProof': self._no_proof
}
for tag, val in bool_map.items():
if val is not None:
r_pr[tag] = 'on' if val else 'off'
return r_pr
def __repr__(self) -> str:
return f"<Font name={self.name!r} bold={self.bold} italic={self.italic}>"
class FontManager:
"""
Централизованный менеджер шрифтов, использующий темы.
Может устанавливать глобальные шрифты (major/minor) для разных языковых групп.
"""
def __init__(self):
self._theme_fonts: Dict[ThemeFont, str] = {}
def set_theme_font(self, theme_type: ThemeFont, font_name: str) -> None:
"""Устанавливает шрифт для определённой темы."""
self._theme_fonts[theme_type] = font_name
def get_theme_font(self, theme_type: ThemeFont) -> Optional[str]:
"""Получает шрифт по типу темы."""
return self._theme_fonts.get(theme_type)
def apply_theme_to_font(self, font: Font) -> None:
"""Применяет тему к объекту Font, если явно не задан основной шрифт."""
# Только если w:ascii не задан, используем w:asciiTheme
if font._ascii is None and font._ascii_theme:
font._ascii = self.get_theme_font(font._ascii_theme)
if font._h_ansi is None and font._h_ansi_theme:
font._h_ansi = self.get_theme_font(font._h_ansi_theme)
if font._east_asia is None and font._east_asia_theme:
font._east_asia = self.get_theme_font(font._east_asia_theme)
if font._cs is None and font._cs_theme:
font._cs = self.get_theme_font(font._cs_theme)
def __repr__(self) -> str:
return f"<FontManager themes={len(self._theme_fonts)}>"
from enum import Enum
from typing import Optional, Dict, Any, List
class StyleType(Enum):
PARAGRAPH = "paragraph"
CHARACTER = "character"
TABLE = "table"
NUMBERING = "numbering"
class Style:
"""
Представление одного стиля Word (<w:style>).
Поддерживает атрибуты: имя, ID, тип, базовый стиль, приоритет, поведение и форматирование.
"""
def __init__(
self,
style_id: str,
type_: StyleType,
name: Optional[str] = None,
default: bool = False,
custom_style: bool = False,
):
self.style_id = style_id
self.type = type_
self.default = default
self.custom_style = custom_style
# Основные свойства
self._name: Optional[str] = name or style_id # fallback на styleId
self._based_on: Optional[str] = None # ссылка на другой styleId
self._next: Optional[str] = None
self._link: Optional[str] = None
# Поведенческие флаги
self._ui_priority: Optional[int] = None
self._semi_hidden: bool = False
self._unhide_when_used: bool = False
self._q_format: bool = False
self._locked: bool = False
# Форматирование
self._paragraph_format: Optional[Dict[str, Any]] = None
self._font: Optional[Dict[str, Any]] = None
self._table_properties: Optional[Dict[str, Any]] = None
self._row_properties: Optional[Dict[str, Any]] = None
self._cell_properties: Optional[Dict[str, Any]] = None
self._table_style_properties: List[Dict[str, Any]] = []
# Служебное
self._rsid: Optional[str] = None
# --- Свойства ---
@property
def name(self) -> str:
return self._name or self.style_id
@name.setter
def name(self, value: str) -> None:
self._name = value
@property
def based_on(self) -> Optional[str]:
return self._based_on
@based_on.setter
def based_on(self, style_id: Optional[str]) -> None:
self._based_on = style_id
@property
def next_style(self) -> Optional[str]:
return self._next
@next_style.setter
def next_style(self, style_id: Optional[str]) -> None:
self._next = style_id
@property
def linked_style(self) -> Optional[str]:
return self._link
@linked_style.setter
def linked_style(self, style_id: Optional[str]) -> None:
self._link = style_id
@property
def priority(self) -> Optional[int]:
return self._ui_priority
@priority.setter
def priority(self, value: Optional[int]) -> None:
if value is not None and not isinstance(value, int):
raise ValueError("priority must be an integer or None")
self._ui_priority = value
@property
def semi_hidden(self) -> bool:
return self._semi_hidden
@semi_hidden.setter
def semi_hidden(self, value: bool) -> None:
self._semi_hidden = bool(value)
@property
def unhide_when_used(self) -> bool:
return self._unhide_when_used
@unhide_when_used.setter
def unhide_when_used(self, value: bool) -> None:
self._unhide_when_used = bool(value)
@property
def quick_style(self) -> bool:
return self._q_format
@quick_style.setter
def quick_style(self, value: bool) -> None:
self._q_format = bool(value)
@property
def locked(self) -> bool:
return self._locked
@locked.setter
def locked(self, value: bool) -> None:
self._locked = bool(value)
# --- Форматирование ---
def set_paragraph_format(self, ppr: Dict[str, Any]) -> None:
self._paragraph_format = ppr
def set_font(self, rpr: Dict[str, Any]) -> None:
self._font = rpr
def set_table_properties(self, tbl_pr: Dict[str, Any]) -> None:
self._table_properties = tbl_pr
def set_row_properties(self, tr_pr: Dict[str, Any]) -> None:
self._row_properties = tr_pr
def set_cell_properties(self, tc_pr: Dict[str, Any]) -> None:
self._cell_properties = tc_pr
def add_table_style_property(self, prop: Dict[str, Any]) -> None:
self._table_style_properties.append(prop)
# --- Вспомогательные методы ---
def is_builtin(self) -> bool:
"""Возвращает True, если стиль встроенный (не пользовательский)."""
return not self.custom_style
def is_visible_in_gallery(self) -> bool:
"""Появляется ли стиль в галерее стилей?"""
return not self.semi_hidden and self.quick_style
def to_xml_dict(self) -> Dict[str, Any]:
"""Сериализация в словарь, близкий к XML."""
result = {
"w:type": self.type.value,
"w:styleId": self.style_id,
}
if self.default:
result["w:default"] = "1"
if self.custom_style:
result["w:customStyle"] = "1"
children = []
if self._name:
children.append({"w:name": {"w:val": self._name}})
if self._based_on:
children.append({"w:basedOn": {"w:val": self._based_on}})
if self._next:
children.append({"w:next": {"w:val": self._next}})
if self._link:
children.append({"w:link": {"w:val": self._link}})
if self._ui_priority is not None:
children.append({"w:uiPriority": {"w:val": str(self._ui_priority)}})
if self._semi_hidden:
children.append({"w:semiHidden": {}})
if self._unhide_when_used:
children.append({"w:unhideWhenUsed": {}})
if self._q_format:
children.append({"w:qFormat": {}})
if self._locked:
children.append({"w:locked": {}})
if self._rsid:
children.append({"w:rsid": {"w:val": self._rsid}})
if self._paragraph_format:
children.append({"w:pPr": self._paragraph_format})
if self._font:
children.append({"w:rPr": self._font})
if self._table_properties:
children.append({"w:tblPr": self._table_properties})
if self._row_properties:
children.append({"w:trPr": self._row_properties})
if self._cell_properties:
children.append({"w:tcPr": self._cell_properties})
for tsp in self._table_style_properties:
children.append({"w:tblStylePr": tsp})
if children:
result["_children"] = children
return result
def __repr__(self) -> str:
return f"<Style id='{self.style_id}' type={self.type.name} name='{self.name}'>"
from typing import Iterator, Optional, Dict, List
import re
class Styles:
"""
Коллекция всех стилей документа: определённых, встроенных и latent.
Имитирует поведение Word по сортировке, отображению и разрешению стилей.
"""
def __init__(self):
self._styles: Dict[str, Style] = {} # styleId -> Style
self._latent_styles: Dict[str, Dict[str, Any]] = {}
self._doc_defaults = {
"rPr": {},
"pPr": {}
}
self._latent_count = 0
self._def_semi_hidden = True
self._def_unhide_when_used = True
self._def_q_format = False
self._def_locked_state = False
self._def_ui_priority = 99
# --- Управление latent styles ---
def define_latent_style(
self,
name: str,
ui_priority: Optional[int] = None,
semi_hidden: Optional[bool] = None,
unhide_when_used: Optional[bool] = None,
q_format: Optional[bool] = None,
locked: Optional[bool] = None,
) -> None:
"""Добавляет исключение для latent style."""
self._latent_styles[name] = {
"uiPriority": ui_priority,
"semiHidden": semi_hidden,
"unhideWhenUsed": unhide_when_used,
"qFormat": q_format,
"locked": locked,
}
def set_latent_defaults(
self,
count: int = 276,
def_semi_hidden: bool = True,
def_unhide_when_used: bool = True,
def_q_format: bool = False,
def_locked_state: bool = False,
def_ui_priority: int = 99,
) -> None:
"""Устанавливает параметры по умолчанию для latent styles."""
self._latent_count = count
self._def_semi_hidden = def_semi_hidden
self._def_unhide_when_used = def_unhide_when_used
self._def_q_format = def_q_format
self._def_locked_state = def_locked_state
self._def_ui_priority = def_ui_priority
def get_latent_behavior(self, name: str) -> Dict[str, Any]:
"""Возвращает поведенческие атрибуты для встроенного стиля."""
base = {
"semiHidden": self._def_semi_hidden,
"unhideWhenUsed": self._def_unhide_when_used,
"qFormat": self._def_q_format,
"locked": self._def_locked_state,
"uiPriority": self._def_ui_priority,
}
override = self._latent_styles.get(name, {})
base.update({k: v for k, v in override.items() if v is not None})
return base
# --- Добавление/получение стилей ---
def add_style(
self,
style_id: str,
type_: StyleType,
name: Optional[str] = None,
default: bool = False,
custom_style: bool = False,
) -> Style:
"""Создаёт и добавляет новый стиль."""
style = Style(style_id, type_, name=name, default=default, custom_style=custom_style)
self._styles[style_id] = style
return style
def get_by_id(self, style_id: str) -> Optional[Style]:
"""Получить стиль по ID."""
return self._styles.get(style_id)
def get_by_name(self, name: str) -> Optional[Style]:
"""Найти стиль по имени (регистронезависимо)."""
for style in self._styles.values():
if style.name.lower() == name.lower():
return style
return None
def __getitem__(self, key: str) -> Style:
"""Доступ как по ID, так и по имени."""
style = self.get_by_id(key)
if style is None:
style = self.get_by_name(key)
if style is None:
raise KeyError(f"Style '{key}' not found.")
return style
def __setitem__(self, key: str, style: Style) -> None:
if not isinstance(style, Style):
raise TypeError("Only Style objects can be added.")
self._styles[style.style_id] = style
def __delitem__(self, key: str) -> None:
style = self.get_by_id(key) or self.get_by_name(key)
if style is None:
raise KeyError(f"Style '{key}' not found.")
del self._styles[style.style_id]
def __contains__(self, key: str) -> bool:
return (key in self._styles) or (self.get_by_name(key) is not None)
def __len__(self) -> int:
return len(self._styles)
def __iter__(self) -> Iterator[Style]:
"""Итерация по всем определённым стилям."""
return iter(self._styles.values())
# --- Document defaults ---
def set_default_run_properties(self, rpr: Dict[str, Any]) -> None:
self._doc_defaults["rPr"] = rpr
def set_default_paragraph_properties(self, ppr: Dict[str, Any]) -> None:
self._doc_defaults["pPr"] = ppr
def get_default_font(self) -> Dict[str, Any]:
return self._doc_defaults["rPr"]
def get_default_paragraph_format(self) -> Dict[str, Any]:
return self._doc_defaults["pPr"]
# --- Списки стилей (как в интерфейсе Word) ---
def recommended_list(self) -> List[Style]:
"""Стили, отображаемые в категории 'Recommended'."""
return [
s for s in self._styles.values()
if not s.semi_hidden
]
def styles_in_use(self, used_style_ids: List[str]) -> List[Style]:
"""Стили, применённые в документе и не semiHidden."""
return [
s for s in self._styles.values()
if s.style_id in used_style_ids and not s.semi_hidden
]
def in_current_document(self) -> List[Style]:
"""Все определённые стили, кроме semiHidden."""
return [s for s in self._styles.values() if not s.semi_hidden]
def all_styles(self) -> List[Style]:
"""Все определённые стили (включая semiHidden)."""
return list(self._styles.values())
# --- Сортировка ---
def sorted_by_appearance(self) -> List[Style]:
"""
Сортировка стилей так, как они отображаются в панели стилей Word:
- По uiPriority (возрастание)
- При равном приоритете — по имени (алфавитно)
- Custom-стили без uiPriority попадают в начало (uiPriority=0)
"""
def sort_key(style: Style) -> tuple:
prio = style.priority if style.priority is not None else 0
return (prio, style.name.lower())
return sorted(self._styles.values(), key=sort_key)
# --- Генерация XML ---
def to_xml_dict(self) -> Dict[str, Any]:
result = {
"w:styles": {
"_children": []
}
}
# docDefaults
if any(self._doc_defaults["rPr"]) or any(self._doc_defaults["pPr"]):
defaults = {"_children": []}
if self._doc_defaults["rPr"]:
defaults["_children"].append({
"w:rPrDefault": {
"_children": [{"w:rPr": self._doc_defaults["rPr"]}]
}
})
if self._doc_defaults["pPr"]:
defaults["_children"].append({
"w:pPrDefault": {
"_children": [{"w:pPr": self._doc_defaults["pPr"]}]
}
})
result["w:styles"]["_children"].append(defaults)
# latentStyles
latent_children = []
for name, props in self._latent_styles.items():
exc = {"w:name": name}
if props["uiPriority"] is not None:
exc["w:uiPriority"] = str(props["uiPriority"])
if props["semiHidden"] is not None:
exc["w:semiHidden"] = "1" if props["semiHidden"] else "0"
if props["unhideWhenUsed"] is not None:
exc["w:unhideWhenUsed"] = "1" if props["unhideWhenUsed"] else "0"
if props["qFormat"] is not None:
exc["w:qFormat"] = "1" if props["qFormat"] else "0"
if props["locked"] is not None:
exc["w:locked"] = "1" if props["locked"] else "0"
latent_children.append({"w:lsdException": exc})
latent_attrs = {
"w:count": str(self._latent_count),
"w:defSemiHidden": "1" if self._def_semi_hidden else "0",
"w:defUnhideWhenUsed": "1" if self._def_unhide_when_used else "0",
"w:defQFormat": "1" if self._def_q_format else "0",
"w:defLockedState": "1" if self._def_locked_state else "0",
"w:defUIPriority": str(self._def_ui_priority),
}
result["w:styles"]["_children"].append({
"w:latentStyles": {
"_children": latent_children,
**latent_attrs
}
})
# Все стили
for style in self._styles.values():
result["w:styles"]["_children"].append({"w:style": style.to_xml_dict()})
return result
def __repr__(self) -> str:
return f"<Styles count={len(self)} latent={len(self._latent_styles)}>"
# Создаём коллекцию стилей
styles = Styles()
# Настройка latent-стилей (встроенные по умолчанию)
styles.set_latent_defaults(count=276, def_semi_hidden=True, def_unhide_when_used=True)
styles.define_latent_style("Normal", ui_priority=0, semi_hidden=False, unhide_when_used=False, q_format=True)
styles.define_latent_style("Heading 1", ui_priority=9, q_format=True)
# Добавляем стили
normal = styles.add_style("Normal", StyleType.PARAGRAPH, name="Normal", default=True)
normal.quick_style = True
normal.semi_hidden = False
heading1 = styles.add_style("Heading1", StyleType.PARAGRAPH, name="Heading 1")
heading1.based_on = "Normal"
heading1.priority = 9
heading1.quick_style = True
heading1.semi_hidden = False
heading1.set_font({"b": {}, "sz": {"w:val": "28"}})
foobar = styles.add_style("Foobar", StyleType.PARAGRAPH, name="Foobar", custom_style=True)
foobar.priority = 50
foobar.semi_hidden = False
foobar.quick_style = True
# Установка умолчаний документа
styles.set_default_run_properties({
"rFonts": {"w:asciiTheme": "minorHAnsi"},
"sz": {"w:val": "24"}
})
styles.set_default_paragraph_properties({"spacing": {"w:after": "120"}})
# Получение стилей
print(styles["Normal"]) # <Style id='Normal' ...>
print(styles["Heading 1"]) # найдёт по имени
# Список рекомендованных
for s in styles.recommended_list():
print(s.name)
# Сортировка
for s in styles.sorted_by_appearance():
print(f"{s.name} (priority={s.priority})")
# Экспорт
xml_data = styles.to_xml_dict()