/
ksrk
/
layout_validation
Обзор
Документация
Войти
/
ksrk
/
layout_validation
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
draft-ui
app/core/services.py
426 строк
16 KB
ksrk
[ui] use adaptive width instead fixed values in st.image for report, layer and form preview
12 авг 2026, 19:13
12 авг 2026, 19:13
f178e19
Код
Авторство
О чём код?
import logging import re from typing import Tuple, Any, List, Dict, Optional, Set import fitz import numpy as np from app.core.document import DocumentWrapper, LayerName from app.core.report import DocumentReport from checks.colors import ColorsCountCheck, CompositeColorCheck from core.entities import CMYK from core.form import PrintFormManager, PrintForm from core.parser import ParsedDrawingElement, PDFStreamParser from core.utils.color import match_color_delta_e_or_poly, is_polygraphic_cmyk_match, \ is_cmyk_tint_or_gcr_match, cmyk_to_hex_lab logger = logging.getLogger(__name__) PANTONE_MATCH_THRESHOLD = 10.0 CMYK_INDEX_MAP = { 0: CMYK.Cyan, 1: CMYK.Magenta, 2: CMYK.Yellow, 3: CMYK.Black, } class ColorValidator: """Сервис для проверки цветовой схемы страницы (число цветов, составной черный/белый).""" def __init__(self, max_color_count: int): self.max_color_count = max_color_count self.colors_count_check = ColorsCountCheck() self.composite_color_check = CompositeColorCheck() def validate_page( self, page: fitz.Page, document_wrapper: DocumentWrapper, page_idx: int, report: DocumentReport ) -> Tuple[dict, Any]: """Запускает проверки цветов, добавляет результаты в отчет и возвращает color_info.""" colors_count_report = self.colors_count_check.run( page, document_wrapper=document_wrapper, page_idx=page_idx, max_color_count=self.max_color_count ) report.add(colors_count_report) report_composite = self.composite_color_check.run( page, document_wrapper=document_wrapper, page_idx=page_idx ) report.add(report_composite) stage_results = { self.colors_count_check.name: colors_count_report, self.composite_color_check.name: report_composite } return stage_results, colors_count_report.check_detail class PrintFormBuilder: """Распределяет элементы страницы по печатным формам без генерации масок.""" @staticmethod def _norm_cmyk(cmyk: Any) -> Optional[Tuple[float, float, float, float]]: if not cmyk or len(cmyk) < 4: return None c, m, y, k = float(cmyk[0]), float(cmyk[1]), float(cmyk[2]), float(cmyk[3]) if max(c, m, y, k) <= 1.0 and (c > 0 or m > 0 or y > 0 or k > 0): c, m, y, k = c * 100.0, m * 100.0, y * 100.0, k * 100.0 return c, m, y, k @staticmethod def _has_ink(cmyk: Tuple[float, float, float, float]) -> bool: return any(v > 0 for v in cmyk) @staticmethod def _sanitize_lab(lab: Any) -> Optional[np.ndarray]: if lab is None: return None if isinstance(lab, set): lab = list(lab) try: lab_list = [float(x) for x in lab] if len(lab_list) >= 3: return np.array(lab_list[:3], dtype=np.float64) except (TypeError, ValueError): pass return None def _extract_spot_from_xref(self, doc: fitz.Document, xref: int) -> Optional[str]: """Раскрывает косвенные ссылки /ColorSpace и извлекает имя Spot/Separation.""" if xref <= 0: return None try: obj_str = doc.xref_object(xref) # Раскрываем косвенные ссылки /ColorSpace N 0 R cs_refs = re.findall(r'/ColorSpace\s+(\d+)\s+0\s+R', obj_str) combined_str = obj_str for cs_xref in cs_refs: try: combined_str += "\n" + doc.xref_object(int(cs_xref)) except Exception: pass if "/Separation" in combined_str or "/DeviceN" in combined_str: match = re.search(r'/(?:Separation|DeviceN)\s+/(?:[^\s/\[\]]+)?\s*/?([^\s/\[\]]+)', combined_str) if match: raw_name = match.group(1) decoded_name = re.sub(r'#([0-9A-Fa-f]{2})', lambda m: chr(int(m.group(1), 16)), raw_name) if decoded_name not in ("DeviceCMYK", "DeviceRGB", "DeviceGray", "All", "None"): return decoded_name match_pantone = re.search(r'/(PANTONE[^\s/\[\]]+)', combined_str, re.IGNORECASE) if match_pantone: return re.sub(r'#([0-9A-Fa-f]{2})', lambda m: chr(int(m.group(1), 16)), match_pantone.group(1)) except Exception: pass return None @staticmethod def _match_pantone( cmyk: Tuple[float, float, float, float], known_pantones: List[Any] ) -> Optional[str]: if not known_pantones or not cmyk: return None cmyk_100 = tuple( float(v * 100.0) if max(cmyk) <= 1.0 else float(v) for v in cmyk ) cmyk_int = tuple(int(round(v)) for v in cmyk_100) u_hex, u_lab = cmyk_to_hex_lab(cmyk_100) for p in known_pantones: p_lab = getattr(p, "lab", None) or (p.get("lab") if isinstance(p, dict) else None) p_cmyk = ( getattr(p, "cmyk_decomposition", None) or getattr(p, "cmyk", None) or (p.get("cmyk_decomposition") if isinstance(p, dict) else ( p.get("cmyk") if isinstance(p, dict) else None)) ) p_cmyk_100: Optional[Tuple[float, float, float, float]] = None p_cmyk_int: Optional[Tuple[int, int, int, int]] = None if p_cmyk: p_cmyk_100 = tuple( float(v * 100.0) if max(p_cmyk) <= 1.0 else float(v) for v in p_cmyk ) p_cmyk_int = tuple(int(round(v)) for v in p_cmyk_100) is_matched = False if p_lab and u_lab: if match_color_delta_e_or_poly( p_lab=u_lab, target_lab=p_lab, p_cmyk_norm=cmyk_100, target_cmyk=p_cmyk_int ): is_matched = True if not is_matched and p_cmyk_100 and p_cmyk_int: if is_polygraphic_cmyk_match(cmyk_100, p_cmyk_int) or is_cmyk_tint_or_gcr_match(cmyk_int, p_cmyk_100): is_matched = True if is_matched: if hasattr(p, "name"): return p.name elif isinstance(p, dict) and "name" in p: return p["name"] return str(p) return None def _get_element_target_plates( self, el: ParsedDrawingElement, known_pantones: List[Any], page: Optional[fitz.Page] = None ) -> Tuple[Set[str], Set[int]]: active_pantones: Set[str] = set() active_cmyk: Set[int] = set() if getattr(el, "is_shading", False): shading_colors = getattr(el, "shading_colors", None) or [] for item in shading_colors: spot_name = item.get("spot_name") cmyk_tuple = item.get("cmyk") if spot_name: matched = False for p in known_pantones: p_name = p.name if hasattr(p, "name") else (p.get("name") if isinstance(p, dict) else str(p)) if p_name == spot_name or p_name in spot_name or spot_name in p_name: active_pantones.add(p_name) matched = True if not matched: active_pantones.add(spot_name) if not active_pantones and cmyk_tuple: if matched := self._match_pantone(cmyk_tuple, known_pantones): active_pantones.add(matched) if not active_pantones: for item in shading_colors: cmyk_tuple = item.get("cmyk") if cmyk_tuple: for idx, val in enumerate(cmyk_tuple): if val > 0: active_cmyk.add(idx) return active_pantones, active_cmyk fill_spot = getattr(el, "fill_spot_name", None) stroke_spot = getattr(el, "stroke_spot_name", None) if getattr(el, "is_image", False) and not fill_spot and page: xref = getattr(el, "xref", 0) extracted_spot = self._extract_spot_from_xref(page.parent, xref) if extracted_spot: fill_spot = extracted_spot setattr(el, "fill_spot_name", fill_spot) fill_cmyk = getattr(el, "fill_cmyk", None) stroke_cmyk = getattr(el, "stroke_cmyk", None) if fill_spot: matched = False for p in known_pantones: p_name = p.name if hasattr(p, "name") else (p.get("name") if isinstance(p, dict) else str(p)) if p_name == fill_spot or p_name in fill_spot or fill_spot in p_name: active_pantones.add(p_name) matched = True if not matched: active_pantones.add(fill_spot) if stroke_spot: matched = False for p in known_pantones: p_name = p.name if hasattr(p, "name") else (p.get("name") if isinstance(p, dict) else str(p)) if p_name == stroke_spot or p_name in stroke_spot or stroke_spot in p_name: active_pantones.add(p_name) matched = True if not matched: active_pantones.add(stroke_spot) if not active_pantones: if fill_cmyk and (matched := self._match_pantone(fill_cmyk, known_pantones)): active_pantones.add(matched) if stroke_cmyk and (matched := self._match_pantone(stroke_cmyk, known_pantones)): active_pantones.add(matched) if not active_pantones: if fill_cmyk: for idx, val in enumerate(fill_cmyk): if val > 0: active_cmyk.add(idx) if stroke_cmyk: for idx, val in enumerate(stroke_cmyk): if val > 0: active_cmyk.add(idx) return active_pantones, active_cmyk @staticmethod def _is_unknown_pantone(name: Optional[str]) -> bool: if not name: return True s = str(name).strip().lower() return "unknown" in s def build_from_page( self, page: fitz.Page, document_wrapper: Any, color_info: Any ) -> Dict[str, PrintForm]: form_manager = PrintFormManager(page) known_pantones = getattr(color_info, "pantone_colors", []) or [] all_page_elements: List[ParsedDrawingElement] = [] found_pantones: Set[str] = set() smask_map = {img_tuple[0]: img_tuple[1] for img_tuple in page.get_images(full=True)} document_wrapper.set_only([LayerName.IMAGES_CMYK, LayerName.PANTONE], page.number) parser = PDFStreamParser(page) for idx, el in enumerate(parser.iter_enriched_drawings()): active_pantones, active_cmyk_indices = self._get_element_target_plates(el, known_pantones, page=page) valid_pantones = {p for p in active_pantones if not self._is_unknown_pantone(p)} if valid_pantones: active_pantones = valid_pantones active_cmyk_indices = set() else: active_pantones = set() if not active_cmyk_indices: active_cmyk_indices = {0, 1, 2, 3} setattr(el, "_target_pantones", active_pantones) setattr(el, "_target_cmyk", active_cmyk_indices) if getattr(el, "is_image", False): xref = getattr(el, "xref", 0) if xref in smask_map and not getattr(el, "mask_xref", 0): setattr(el, "mask_xref", smask_map[xref]) found_pantones.update(active_pantones) all_page_elements.append(el) existing_image_xrefs = { getattr(el, "xref", 0) for el in all_page_elements if getattr(el, "is_image", False) } start_img_idx = len(all_page_elements) for img_idx, img_info in enumerate(page.get_image_info(xrefs=True), start=start_img_idx): img_bbox = fitz.Rect(img_info["bbox"]) if img_bbox.is_empty or img_bbox.width <= 0 or img_bbox.height <= 0: continue xref = img_info.get("xref", 0) or img_info.get("number", 0) if xref in existing_image_xrefs: continue spot_name = self._extract_spot_from_xref(page.parent, xref) target_pantones: Set[str] = set() target_cmyk: Set[int] = set() if spot_name and not self._is_unknown_pantone(spot_name): matched = False for p in known_pantones: p_name = p.name if hasattr(p, "name") else (p.get("name") if isinstance(p, dict) else str(p)) if p_name == spot_name or p_name in spot_name or spot_name in p_name: if not self._is_unknown_pantone(p_name): target_pantones.add(p_name) matched = True if not matched: target_pantones.add(spot_name) target_pantones = {p for p in target_pantones if not self._is_unknown_pantone(p)} if target_pantones: found_pantones.update(target_pantones) layer_name = LayerName.PANTONE.value if hasattr(LayerName, "PANTONE") else LayerName.IMAGES_CMYK.value else: layer_name = LayerName.IMAGES_CMYK.value obj_str = "" if xref > 0: try: obj_str = page.parent.xref_object(xref) except Exception: pass if not ("/DeviceGray" in obj_str or "/CalGray" in obj_str or "/ImageMask" in obj_str): target_cmyk = {0, 1, 2, 3} img_el = ParsedDrawingElement( index=img_idx, drawing={}, layer_name=layer_name ) setattr(img_el, "is_image", True) setattr(img_el, "fill_spot_name", spot_name) setattr(img_el, "rect", img_bbox) setattr(img_el, "xref", xref) setattr(img_el, "mask_xref", smask_map.get(xref, 0)) setattr(img_el, "_target_pantones", target_pantones) setattr(img_el, "_target_cmyk", target_cmyk) all_page_elements.append(img_el) valid_found_pantones = {p for p in found_pantones if not self._is_unknown_pantone(p)} for p_name in valid_found_pantones: pantone_elements = [ el for el in all_page_elements if p_name in getattr(el, "_target_pantones", set()) ] if pantone_elements: form_manager.add_form( name=f"Pantone_{p_name}", form_type="PANTONE", elements=pantone_elements, all_page_elements=all_page_elements, ) for idx, ch_enum in CMYK_INDEX_MAP.items(): ch_elements = [ el for el in all_page_elements if idx in getattr(el, "_target_cmyk", set()) ] form_manager.add_form( name=ch_enum.value, form_type="CMYK", elements=ch_elements, all_page_elements=all_page_elements, cmyk_channel=ch_enum, ) special_layers = [LayerName.WHITEWASH, LayerName.VARNISH] for s_layer_enum in special_layers: if document_wrapper.has_layer_type(s_layer_enum): s_name = s_layer_enum.value form_manager.add_special_form( name=s_name, form_type=s_name, document_wrapper=document_wrapper, layer_enum=s_layer_enum, ) return form_manager.forms