This commit is contained in:
2026-03-12 01:50:57 +05:00
parent ec82b528a3
commit c33befe7c2
12 changed files with 1553 additions and 69 deletions

View File

@@ -2,8 +2,11 @@
import copy
import datetime
import io
from openpyxl import load_workbook
from openpyxl.drawing.image import Image as XlImage
from openpyxl.drawing.spreadsheet_drawing import OneCellAnchor, AnchorMarker
from openpyxl.styles import Font
from services.data_loader import ki_val
@@ -29,10 +32,14 @@ TMPL = dict(
OBOSN_ZU=33, # F33: обоснование ЗУ
ZU_COORD_START=37, # первая строка данных координат ЗУ
ZU_COORD_PRE=5, # кол-во предзаполненных строк (37-41)
LOC_OKS=54, # F54: местоположение ОКС (до смещения)
OBOSN_OKS=58, # F58: обоснование ОКС (до смещения)
OKS_COORD_START=62, # первая строка данных ОКС (до смещения)
OKS_COORD_PRE=1, # кол-во предзаполненных строк ОКС
ZU_SCHEME_START=42, # строка-заголовок раздела 6 (до смещения)
ZU_SCHEME_PRE=1, # r43 = изображение, r44 = масштаб, r45-r50 = легенда
LOC_OKS=54, # F54: местоположение ОКС (до смещения)
OBOSN_OKS=58, # F58: обоснование ОКС (до смещения)
OKS_COORD_START=62, # первая строка данных ОКС (до смещения)
OKS_COORD_PRE=1, # кол-во предзаполненных строк ОКС
OKS_SCHEME_START=63, # строка-заголовок раздела 9 «9. Схема ОКС» (до смещения)
OKS_SCHEME_PRE=1, # r64 = изображение, r65 = масштаб
)
# ──────────────────────────── Вспомогательные ───────────────────────────── #
@@ -69,6 +76,58 @@ def _safe_insert_rows(ws, idx: int, amount: int):
)
def _safe_delete_rows(ws, idx: int, amount: int):
"""
Удалить `amount` строк начиная с `idx`, корректно сдвинув merge-диапазоны.
Диапазоны, целиком попавшие в удаляемый участок, удаляются.
Диапазоны, пересекающие границу — обрезаются/сдвигаются.
"""
end_idx = idx + amount # первая строка ПОСЛЕ удаляемого блока
to_remove = []
to_readjust = []
for rng in list(ws.merged_cells.ranges):
mn, mc, mx, xc = rng.min_row, rng.min_col, rng.max_row, rng.max_col
if mx < idx:
# полностью до зоны удаления — ничего не трогаем
continue
to_remove.append(rng) # нужно снять, а затем, возможно, заново добавить
if mn >= end_idx:
# полностью после — просто сдвигаем вверх
to_readjust.append((mn - amount, mc, mx - amount, xc))
elif mn >= idx and mx < end_idx:
# полностью внутри удаляемого — удаляем без восстановления
pass
elif mn < idx and mx < end_idx:
# начинается до, заканчивается внутри — обрезаем конец
new_mx = idx - 1
if new_mx >= mn:
to_readjust.append((mn, mc, new_mx, xc))
elif mn < idx and mx >= end_idx:
# охватывает весь удаляемый блок — уменьшаем
to_readjust.append((mn, mc, mx - amount, xc))
else:
# начинается внутри, заканчивается после — обрезаем начало
new_mn = idx
to_readjust.append((new_mn, mc, mx - amount, xc))
for rng in to_remove:
ws.merged_cells.remove(rng)
ws.delete_rows(idx, amount)
for min_r, min_c, max_r, max_c in to_readjust:
if min_r <= max_r:
ws.merge_cells(
start_row=min_r, start_column=min_c,
end_row=max_r, end_column=max_c,
)
def _copy_row_style(ws, src_row: int, dst_row: int):
for col in range(1, ws.max_column + 1):
src = ws.cell(row=src_row, column=col)
@@ -120,8 +179,66 @@ def _apply_font_all(ws):
# ─────────────────────────────── Публичная функция ──────────────────────── #
def _embed_image(ws, png_bytes: bytes, anchor_row: int, pre_rows: int) -> int:
"""
Вставить PNG ровно в ОДНУ ячейку (строку) начиная с A{anchor_row}.
Стратегия «одна ячейка»:
Все pre_rows строк шаблона, зарезервированных под схему, кроме первой,
удаляются (_safe_delete_rows), чтобы осталась единственная объединённая
строка A{anchor_row}:F{anchor_row}.
• Высота этой строки выставляется равной реальной высоте PNG (pt).
• TwoCellAnchor с editAs='twoCell' привязывает изображение точно к этой
одной строке — от её верхнего левого угла до нижнего правого.
Возвращает изменение числа строк (всегда -(pre_rows-1), т.е. отрицательное).
"""
from PIL import Image as PILImage
pil = PILImage.open(io.BytesIO(png_bytes))
px_w, px_h = pil.size
dpi = pil.info.get("dpi", (96, 96))
if not isinstance(dpi, (tuple, list)) or dpi[0] == 0:
dpi = (96, 96)
pt_w = px_w * 72.0 / dpi[0]
pt_h = px_h * 72.0 / dpi[1]
# Масштаб по ширине листа (~420 pt для 6 колонок)
MAX_PT_W = 420.0
if pt_w > MAX_PT_W:
scale = MAX_PT_W / pt_w
pt_w = MAX_PT_W
pt_h *= scale
# Удаляем все строки зоны схемы кроме первой (anchor_row).
# Строка anchor_row уже является частью объединённой ячейки в шаблоне;
# после удаления нижних строк она станет单独ной строкой высотой pt_h.
rows_to_delete = pre_rows - 1
if rows_to_delete > 0:
_safe_delete_rows(ws, anchor_row + 1, rows_to_delete)
# Задаём высоту единственной оставшейся строки = высоте изображения
ws.row_dimensions[anchor_row].height = pt_h
# OneCellAnchor: верхний-левый угол прибит к ячейке A{anchor_row},
# размер задан явно в EMU (1 pt = 12700 EMU).
# В отличие от TwoCellAnchor, LibreOffice не пересчитывает позицию
# через сумму высот строк → картинка не уплывает вверх при склейке форм.
img = XlImage(io.BytesIO(png_bytes))
cell_anchor = OneCellAnchor()
cell_anchor._from = AnchorMarker(col=0, colOff=0, row=anchor_row - 1, rowOff=0)
cell_anchor.ext.cx = int(pt_w * 12700) # pt → EMU
cell_anchor.ext.cy = int(pt_h * 12700)
img.anchor = cell_anchor
ws.add_image(img)
return -rows_to_delete # отрицательное смещение: строки были удалены
def fill_form(template_path: str, output_path: str,
ki: dict, t2_row: tuple, mif_index: dict):
ki: dict, t2_row: tuple, mif_index: dict,
zu_scheme: bytes = None, oks_scheme: bytes = None,
zu_scale: str = None, oks_scale: str = None):
"""Заполнить шаблон данными одной строки Таблицы 2 и сохранить файл."""
def col(i, default=None):
@@ -180,7 +297,16 @@ def fill_form(template_path: str, output_path: str,
zu_coord_data = build_coord_rows(zu_contours, str(method_zu), mt_zu)
zu_offset = _insert_coord_rows(ws, zu_coord_data, T["ZU_COORD_START"], T["ZU_COORD_PRE"])
# Разделы 7-9: ОКС (со смещением)
# Раздел 6 (схема ЗУ):
# r43+offset = изображение (ZU_SCHEME_PRE=1, строки не удаляются)
# r44+offset = масштаб (c1)
if zu_scheme:
scheme_anchor = T["ZU_SCHEME_START"] + zu_offset + 1
_embed_image(ws, zu_scheme, scheme_anchor, T["ZU_SCHEME_PRE"])
if zu_scale:
_sv(ws, scheme_anchor + 1, 1, zu_scale, F)
# Разделы 7-9: ОКС (смещение только от доп. строк координат ЗУ; схема не изменяет кол-во строк)
ofs = zu_offset
_sv(ws, T["LOC_OKS"] + ofs, 6, loc_oks or "-", F)
_sv(ws, T["OBOSN_OKS"] + ofs, 6, obosn_oks or "-", F)
@@ -188,8 +314,17 @@ def fill_form(template_path: str, output_path: str,
oks_path = mif_index.get(str(oks_file).strip()) if oks_file else None
oks_contours = get_contours(oks_path) if oks_path else []
oks_coord_data = build_coord_rows(oks_contours, str(method_oks or ""), mt_oks)
oks_offset = 0
if oks_coord_data:
_insert_coord_rows(ws, oks_coord_data, T["OKS_COORD_START"] + ofs, T["OKS_COORD_PRE"])
oks_offset = _insert_coord_rows(ws, oks_coord_data, T["OKS_COORD_START"] + ofs, T["OKS_COORD_PRE"])
# Раздел 9: схема ОКС (из Таблицы3 по Table2 столбец K «Название файла ОКС»)
# r64+ofs+oks_offset = изображение, r65+ofs+oks_offset = масштаб
if oks_scheme:
oks_scheme_anchor = T["OKS_SCHEME_START"] + ofs + oks_offset + 1
_embed_image(ws, oks_scheme, oks_scheme_anchor, T["OKS_SCHEME_PRE"])
if oks_scale:
_sv(ws, oks_scheme_anchor + 1, 1, oks_scale, F)
_apply_font_all(ws)
wb.save(output_path)