/
ksrk
/
layout_validation
Обзор
Документация
Войти
/
ksrk
/
layout_validation
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
draft-ui
app/core/parser.py
962 строки
31 KB
ksrk
refactored print form class
09 авг 2026, 23:38
09 авг 2026, 23:38
37ebc8e
Код
Авторство
О чём код?
import logging import re from collections import deque, defaultdict from dataclasses import dataclass, field from typing import Set, Tuple, Dict, Optional, Any, Iterator, List from fitz import Page from pymupdf import Point, Rect from core.utils.color import lab_to_cmyk, rgb_to_cmyk logger = logging.getLogger(__name__) _TOKEN_RE = re.compile(r'/[A-Za-z0-9_.-]+|[\d.-]+|[a-zA-Z*]+') _DRAWING_OPS = {'f', 'F', 'f*', 'S', 's', 'B', 'B*', 'b', 'b*', 'sh'} _COLOR_OPS = {'k', 'K', 'sc', 'scn', 'SC', 'SCN', 'g', 'G', 'cs', 'CS'} @dataclass class ParsedDrawingElement: """Обогащенный графический элемент из PDF с привязанным цветовым состоянием и слоем.""" index: int drawing: Dict[str, Any] layer_name: str fill_cmyk: Optional[Tuple[int, int, int, int]] = None fill_spot_name: Optional[str] = None fill_spot_tint: float = 1.0 stroke_cmyk: Optional[Tuple[int, int, int, int]] = None stroke_spot_name: Optional[str] = None stroke_spot_tint: float = 1.0 # Градиентные поля is_shading: bool = False shading_name: Optional[str] = None shading_colors: Optional[List[Dict[str, Any]]] = field(default=None) is_image: bool = False class PDFStreamParser: """ Класс для низкоуровневого анализа контент-потоков PDF, ресурсов, слоев и вегетативных графических операторов (PyMuPDF / raw streams). """ def __init__(self, page: Page): self.page = page self.doc = page.parent self._stream_text: Optional[str] = None self._pantone_map: Optional[dict] = None self._layer_map: Optional[dict] = None self._shading_resources_map: Optional[dict] = None self._active_ocg_names: Optional[Set[str]] = None @property def stream_text(self) -> str | None: """Ленивая загрузка и кэширование контент-стрима страницы.""" if self._stream_text is None: try: self._stream_text = self.page.read_contents().decode('utf-8', errors='ignore') except Exception as e: logger.error(f"Не удалось прочитать стрим страницы {self.page.number}: {e}") self._stream_text = "" return self._stream_text @property def pantone_map(self) -> dict | None: """Ленивая инициализация маппинга цветовых пространств (Spot/Pantone).""" if self._pantone_map is None: self._pantone_map = self.resolve_colorspace_resources() return self._pantone_map @property def layer_map(self) -> dict | None: """Ленивая инициализация маппинга алиасов слоев в их реальные имена.""" if self._layer_map is None: self._layer_map = self.resolve_layer_resources() return self._layer_map @property def shading_resources_map(self) -> dict | None: """Ленивая инициализация словаря градиентных ресурсов (/Shading -> xrefs).""" if self._shading_resources_map is None: self._shading_resources_map = self.resolve_shading_resources() return self._shading_resources_map @property def active_ocg_names(self) -> Set[str]: """Динамическое получение актуального состояния включенных слоев.""" try: return { layer["text"] for layer in self.doc.layer_ui_configs() if layer["on"] } except Exception as e: logger.warning(f"Не удалось получить конфигурацию слоев: {e}") return set() def is_layer_allowed_for_page(self, layer_name: str, allowed_layers: Set[str]) -> bool: """ Проверяет, используется ли слой на текущей странице (нумерация с 1). Поддерживает имена видов IMAGE_CMYK_12 или PANTONE_357 (номера страниц указаны после _). """ if not layer_name: return False if layer_name == "Root" or ("Root" in allowed_layers and layer_name == "Root"): return True current_page_num = self.page.number + 1 page_str = str(current_page_num) match = re.match(r"^(.*?)(?:_([0-9_]+))?$", layer_name) if match: base_name = match.group(1) pages_part = match.group(2) else: base_name = layer_name pages_part = None base_matched = False if layer_name in allowed_layers or base_name in allowed_layers: base_matched = True else: for allowed in allowed_layers: if not allowed: continue allowed_match = re.match(r"^(.*?)(?:_([0-9_]+))?$", allowed) allowed_base = allowed_match.group(1) if allowed_match else allowed if base_name == allowed_base or base_name.startswith(allowed_base) or allowed_base.startswith(base_name): base_matched = True break if not base_matched: return False if not pages_part: return True tokens = pages_part.split('_') if page_str in tokens: return True if page_str in pages_part: return True return False def resolve_colorspace_resources(self) -> dict: """Низкоуровневый маппинг внутренних имен спотовых пространств (/CS0 -> Pantone).""" pantone_mapping = {} if not hasattr(self.page, 'xref') or self.page.xref == 0: return pantone_mapping try: page_text = self.doc.xref_object(self.page.xref) res_text = page_text res_match = re.search(r'/Resources\s+(\d+)\s+0\s+R', page_text) if res_match: res_text = self.doc.xref_object(int(res_match.group(1))) cs_text = "" cs_match = re.search(r'/ColorSpace\s+(\d+)\s+0\s+R', res_text) if cs_match: cs_text = self.doc.xref_object(int(cs_match.group(1))) else: cs_inline_match = re.search(r'/ColorSpace\s*<<([\s\S]*?)>>', res_text) if cs_inline_match: cs_text = cs_inline_match.group(1) if cs_text: cs_links = re.findall(r'/([A-Za-z0-9_.-]+)\s+(\d+)\s+0\s+R', cs_text) for cs_name, xref_str in cs_links: cs_obj_text = self.doc.xref_object(int(xref_str)) if "/Separation" in cs_obj_text: name_match = re.search(r'/Separation\s+/([^ \n\r\]>]+)', cs_obj_text) if name_match: real_name = name_match.group(1).replace('#20', ' ') pantone_mapping[cs_name] = {"real_name": real_name, "base_space": "DeviceCMYK"} except Exception as e: logger.error(f"Ошибка парсинга словаря цветов: {e}") return pantone_mapping def resolve_layer_resources(self) -> dict: """Связывает технические алиасы контент-потока с реальными именами слоев (MC0 -> IMAGE_CMYK).""" layer_mapping = {} if not hasattr(self.page, 'xref') or self.page.xref == 0: return layer_mapping try: page_text = self.doc.xref_object(self.page.xref) res_text = page_text res_match = re.search(r'/Resources\s+(\d+)\s+0\s+R', page_text) if res_match: res_text = self.doc.xref_object(int(res_match.group(1))) prop_text = "" prop_match = re.search(r'/Properties\s+(\d+)\s+0\s+R', res_text) if prop_match: prop_text = self.doc.xref_object(int(prop_match.group(1))) else: prop_inline_match = re.search(r'/Properties\s*<<([\s\S]*?)>>', res_text) if prop_inline_match: prop_text = prop_inline_match.group(1) if prop_text: prop_links = re.findall(r'/([A-Za-z0-9_.-]+)\s+(\d+)\s+0\s+R', prop_text) for prop_name, xref_str in prop_links: ocg_obj_text = self.doc.xref_object(int(xref_str)) if "/OCG" in ocg_obj_text or "/Type/OCG" in ocg_obj_text.replace(" ", ""): name_match = re.search(r'/Name\s*\(([^)]+)\)', ocg_obj_text) if not name_match: name_match = re.search(r'/Name\s*/([^ \n\r>]+)', ocg_obj_text) if name_match: layer_mapping[prop_name] = name_match.group(1).strip() except Exception as e: logger.error(f"Ошибка парсинга словаря свойств слоев: {e}") return layer_mapping def resolve_shading_resources(self) -> Dict[str, int]: """Сопоставляет алиасы градиентов (/Sh1, /Sh2) с их XRef номерами.""" shading_map = {} if not hasattr(self.page, 'xref') or self.page.xref == 0: return shading_map try: page_text = self.doc.xref_object(self.page.xref) res_text = page_text res_match = re.search(r'/Resources\s+(\d+)\s+0\s+R', page_text) if res_match: res_text = self.doc.xref_object(int(res_match.group(1))) shading_match = re.search(r'/Shading\s*<<([\s\S]*?)>>', res_text) if shading_match: links = re.findall(r'/([A-Za-z0-9_.-]+)\s+(\d+)\s+0\s+R', shading_match.group(1)) for sh_alias, xref_str in links: shading_map[sh_alias] = int(xref_str) except Exception as e: logger.error(f"Ошибка парсинга ресурсов Shading: {e}") return shading_map def extract_cmyk_from_layer(self) -> Set[Tuple[int, int, int, int]]: """Извлекает уникальные CMYK цвета из content stream страницы с учетом слоев.""" unique_cmyk_colors = set() allowed_layers = (self.active_ocg_names or set()) | {"Root"} properties_map = self.layer_map streams_to_parse = [self.stream_text] if self.stream_text else [] try: xobject_xrefs = set(re.findall(r"/(?:Im|Form)\d+\s+(\d+)\s+\d+\s+R", self.doc.xref_object(self.page.xref))) for x_xref_str in xobject_xrefs: x_xref = int(x_xref_str) if "/Subtype /Form" in self.doc.xref_object(x_xref): streams_to_parse.append(self.doc.xref_stream(x_xref).decode("latin1", errors="ignore")) except Exception: pass args = deque(maxlen=20) for stream_text in streams_to_parse: token_iter = _TOKEN_RE.finditer(stream_text) layer_stack = [] last_fill_cmyk = None last_stroke_cmyk = None has_geometry = False args.clear() for match in token_iter: token = match.group(0) if token.startswith('/'): args.append(token) elif token[0] in '0123456789.-': try: args.append(float(token)) except ValueError: pass else: op = token if op in ("f", "F", "f*"): cur_layer = layer_stack[-1] if layer_stack else "Root" if self.is_layer_allowed_for_page(cur_layer, allowed_layers) and last_fill_cmyk and has_geometry: unique_cmyk_colors.add(last_fill_cmyk) has_geometry = False args.clear() elif op in ("s", "S"): cur_layer = layer_stack[-1] if layer_stack else "Root" if self.is_layer_allowed_for_page(cur_layer, allowed_layers) and last_stroke_cmyk and has_geometry: unique_cmyk_colors.add(last_stroke_cmyk) has_geometry = False args.clear() elif op in ("b", "B", "b*", "B*"): cur_layer = layer_stack[-1] if layer_stack else "Root" if self.is_layer_allowed_for_page(cur_layer, allowed_layers) and has_geometry: if last_fill_cmyk: unique_cmyk_colors.add(last_fill_cmyk) if last_stroke_cmyk: unique_cmyk_colors.add(last_stroke_cmyk) has_geometry = False args.clear() elif op == "k" and len(args) >= 4: try: last_fill_cmyk = ( round(args[-4] * 100), round(args[-3] * 100), round(args[-2] * 100), round(args[-1] * 100), ) except (ValueError, TypeError): last_fill_cmyk = None args.clear() elif op == "K" and len(args) >= 4: try: last_stroke_cmyk = ( round(args[-4] * 100), round(args[-3] * 100), round(args[-2] * 100), round(args[-1] * 100), ) except (ValueError, TypeError): last_stroke_cmyk = None args.clear() elif op in ("sc", "scn"): if len(args) >= 4: try: last_fill_cmyk = ( round(args[-4] * 100), round(args[-3] * 100), round(args[-2] * 100), round(args[-1] * 100), ) except (ValueError, TypeError): pass args.clear() elif op in ("SC", "SCN"): if len(args) >= 4: try: last_stroke_cmyk = ( round(args[-4] * 100), round(args[-3] * 100), round(args[-2] * 100), round(args[-1] * 100), ) except (ValueError, TypeError): pass args.clear() elif op in ("m", "l", "c", "v", "y"): has_geometry = True args.clear() elif op == "re": if len(args) >= 4: try: if abs(args[-2]) > 0.001 and abs(args[-1]) > 0.001: has_geometry = True except (ValueError, TypeError): has_geometry = True args.clear() elif op == "BDC": if len(args) >= 1: ocg_id = str(args[-1]).lstrip("/") resolved_layer = properties_map.get(ocg_id, ocg_id) layer_stack.append(resolved_layer) args.clear() elif op == "EMC": if layer_stack: layer_stack.pop() args.clear() elif op in ("g", "rg", "cs"): last_fill_cmyk = None args.clear() elif op in ("G", "RG", "CS"): last_stroke_cmyk = None args.clear() else: args.clear() return unique_cmyk_colors def extract_pantone_from_stream(self) -> Dict[str, dict]: """Извлекает из потока страницы описание Pantone/Spot цветов.""" pantone_pattern = re.compile( r"(/Pantone[^\s()<>\[\]{}/%]*)[^<]*<<(.*?)>>", re.IGNORECASE | re.DOTALL ) found_colors = {} try: page_inf = self.doc.tobytes(deflate=False).decode("latin-1", errors="ignore") for match in pantone_pattern.finditer(page_inf): raw_name = match.group(1) dict_content = match.group(2) clean_name = raw_name.replace("/", "").replace("#20", " ").replace("#2d", "-").replace("+", " ") if clean_name.lower().startswith("pantone"): parts = clean_name.split() if len(parts) > 1: clean_name = f"PANTONE {' '.join(parts[1:])}".upper() c1_match = re.search(r"/C1\s*\[\s*([\d\.\s\-]+)\]", dict_content) if not c1_match: continue raw_vals = [float(x) for x in c1_match.group(1).split() if x] if any(x < 0 for x in raw_vals) or "127" in dict_content: detected_space = "DeviceLab" elif len(raw_vals) == 4: detected_space = "DeviceCMYK" elif len(raw_vals) == 3: detected_space = "DeviceRGB" else: detected_space = "Неизвестно" found_colors[clean_name] = { "space": detected_space, "values": raw_vals, } except Exception as e: logger.error(f"Ошибка парсинга страницы: {e}") return found_colors def _resolve_xref_text(self, raw_val: str) -> tuple[str, Optional[int]]: """Загружает текст косвенного объекта XRef, если строка имеет вид 'X 0 R'.""" if not raw_val: return "", None raw_val = raw_val.strip() match = re.match(r"^(\d+)\s+0\s+R$", raw_val) if match: xref = int(match.group(1)) return self.doc.xref_object(xref), xref return raw_val, None def _parse_shading_colors(self, sh_alias: str) -> List[Dict[str, Any]]: """Разбирает указанный ресурс Shading и возвращает ключевые цвета градиента в CMYK.""" xref = self.shading_resources_map.get(sh_alias) if not xref: return [] extracted_nodes = [] try: cs_key = self.doc.xref_get_key(xref, "ColorSpace")[1] func_key = self.doc.xref_get_key(xref, "Function")[1] cs_text, cs_xref = self._resolve_xref_text(cs_key) is_lab = "/Lab" in cs_text or "DeviceLab" in cs_text is_spot = "/Separation" in cs_text is_devicen = "/DeviceN" in cs_text spot_name = None devicen_colorants = [] if is_spot: match = re.search(r'/Separation\s+/([^\s/\[\]]+)', cs_text) if match: spot_name = match.group(1).replace("#20", " ") elif is_devicen: match = re.search(r'/DeviceN\s*\[\s*([^\]]+)\s*\]', cs_text) if match: raw_names = match.group(1).split() devicen_colorants = [ n.lstrip('/').replace("#20", " ") for n in raw_names if n.startswith('/') ] funcs_to_process = [func_key] while funcs_to_process: f_raw = funcs_to_process.pop(0) f_text, f_xref = self._resolve_xref_text(f_raw) if not f_xref: continue f_type = self.doc.xref_get_key(f_xref, "FunctionType")[1] if f_type == "2": c0_str = self.doc.xref_get_key(f_xref, "C0")[1] c1_str = self.doc.xref_get_key(f_xref, "C1")[1] c0_vals = [float(x) for x in c0_str.strip("[]").split()] if c0_str and c0_str != "null" else [0.0] c1_vals = [float(x) for x in c1_str.strip("[]").split()] if c1_str and c1_str != "null" else [1.0] for label, vals in [("C0_start", c0_vals), ("C1_end", c1_vals)]: cmyk_res = None node_spot_name = spot_name norm_vals = [round(v if max(vals) > 1.0 else v * 100.0, 2) for v in vals] if is_lab and len(vals) == 3: cmyk_res = lab_to_cmyk(vals[0], vals[1], vals[2]) elif len(vals) == 4: cmyk_res = tuple(norm_vals) elif is_devicen and devicen_colorants and len(vals) == len(devicen_colorants): cmyk_arr = [0.0, 0.0, 0.0, 0.0] cmyk_map = {"Cyan": 0, "Magenta": 1, "Yellow": 2, "Black": 3} has_cmyk_components = False for col_name, val in zip(devicen_colorants, norm_vals): if col_name in cmyk_map: cmyk_arr[cmyk_map[col_name]] = val has_cmyk_components = True else: node_spot_name = col_name if has_cmyk_components: cmyk_res = tuple(cmyk_arr) elif len(vals) == 2: cmyk_res = (norm_vals[0], norm_vals[1], 0.0, 0.0) elif len(vals) == 1 and not is_spot: cmyk_res = (0.0, 0.0, 0.0, norm_vals[0]) extracted_nodes.append({ "point": label, "raw_values": vals, "cmyk": cmyk_res, "spot_name": node_spot_name }) elif f_type == "3": funcs_str = self.doc.xref_get_key(f_xref, "Functions")[1] sub_funcs = re.findall(r"\d+\s+0\s+R", funcs_str) funcs_to_process.extend(sub_funcs) except Exception as e: logger.error(f"Error parsing shading colors for {sh_alias}: {e}") return extracted_nodes def _get_resource_xref(self, category: str, alias: str) -> Optional[int]: """ Получает xref ресурса (Shading, Pattern, XObject и т.д.) по его алиасу в /Resources страницы. """ doc = self.doc key_path = f"Resources/{category}/{alias}" entry_type, entry_val = doc.xref_get_key(self.page.xref, key_path) if entry_type != "xref": return None return int(entry_val.split()[0]) def _get_shading_xref(self, alias: str) -> Optional[int]: return self._get_resource_xref("Shading", alias) def _build_page_image_map(self) -> Dict[str, deque]: """Собирает карту размещения растровых картинок с их xref.""" img_map = defaultdict(deque) try: for img in self.page.get_images(full=True): name = img[7] if name: img_map[name].append(img) except Exception as e: logger.warning(f"Не удалось извлечь image_info: {e}") return img_map @staticmethod def _extract_cmyk_from_geom(color_tuple) -> Optional[Tuple[int, int, int, int]]: if not color_tuple: return None if len(color_tuple) == 4: return ( int(round(color_tuple[0] * 100)), int(round(color_tuple[1] * 100)), int(round(color_tuple[2] * 100)), int(round(color_tuple[3] * 100)), ) elif len(color_tuple) == 1: k = int(round((1.0 - color_tuple[0]) * 100)) return 0, 0, 0, k elif len(color_tuple) == 3: return rgb_to_cmyk(color_tuple[0], color_tuple[1], color_tuple[2]) return None def iter_enriched_drawings(self) -> Iterator[ParsedDrawingElement]: """ Финальный генератор со стейт-машиной цветов, поддержкой extended=True, изоляцией графического состояния (q/Q) и извлечением растровых изображений (Do). """ drawings = self.page.get_drawings() total_drawings = len(drawings) drawing_idx = 0 image_map = self._build_page_image_map() is_layer_allowed = True allowed_layers = (self.active_ocg_names or set()) | {"Root"} cur_layer = "Root" layer_stack: List[Tuple[str, bool]] = [] cur_fill_cmyk: Optional[Tuple[int, int, int, int]] = None cur_stroke_cmyk: Optional[Tuple[int, int, int, int]] = None cs_map = self.resolve_colorspace_resources() cur_fill_spot: Optional[str] = None cur_fill_tint: float = 1.0 cur_stroke_spot: Optional[str] = None cur_stroke_tint: float = 1.0 gstate_stack: List[Dict[str, Any]] = [] current_path_items: List[Tuple[Any, ...]] = [] last_clip_items: List[Tuple[Any, ...]] = [] current_point: Optional[Point] = None args = deque(maxlen=20) token_iter = _TOKEN_RE.finditer(self.stream_text) page_h = self.page.rect.height for match in token_iter: token = match.group(0) if token.startswith("/"): args.append(token) elif token[0] in "0123456789.-": try: args.append(float(token)) except ValueError: pass else: op = token if op == "q": gstate_stack.append({ "fill_cmyk": cur_fill_cmyk, "fill_spot": cur_fill_spot, "fill_tint": cur_fill_tint, "stroke_cmyk": cur_stroke_cmyk, "stroke_spot": cur_stroke_spot, "stroke_tint": cur_stroke_tint, }) args.clear() continue elif op == "Q": if gstate_stack: state = gstate_stack.pop() cur_fill_cmyk = state["fill_cmyk"] cur_fill_spot = state["fill_spot"] cur_fill_tint = state["fill_tint"] cur_stroke_cmyk = state["stroke_cmyk"] cur_stroke_spot = state["stroke_spot"] cur_stroke_tint = state["stroke_tint"] args.clear() continue if op in ("BT", "ET"): current_path_items.clear() current_point = None args.clear() continue if op == "m" and len(args) >= 2: current_point = Point(args[-2], page_h - args[-1]) elif op == "l" and len(args) >= 2: pt = Point(args[-2], page_h - args[-1]) p0 = current_point if current_point is not None else pt current_path_items.append(("l", p0, pt)) current_point = pt elif op == "c" and len(args) >= 6: p1 = Point(args[-6], page_h - args[-5]) p2 = Point(args[-4], page_h - args[-3]) p3 = Point(args[-2], page_h - args[-1]) p0 = current_point if current_point is not None else p1 current_path_items.append(("c", p0, p1, p2, p3)) current_point = p3 elif op == "v" and len(args) >= 4: p2 = Point(args[-4], page_h - args[-3]) p3 = Point(args[-2], page_h - args[-1]) p0 = current_point if current_point is not None else p2 current_path_items.append(("c", p0, p0, p2, p3)) current_point = p3 elif op == "y" and len(args) >= 4: p1 = Point(args[-4], page_h - args[-3]) p3 = Point(args[-2], page_h - args[-1]) p0 = current_point if current_point is not None else p1 current_path_items.append(("c", p0, p1, p3, p3)) current_point = p3 elif op == "re" and len(args) >= 4: try: x, y, w, h = args[-4], args[-3], args[-2], args[-1] r = Rect(x, page_h - (y + h), x + w, page_h - y) current_path_items.append(("re", r)) except Exception: pass current_point = None if op in ("W", "W*"): if current_path_items: last_clip_items = list(current_path_items) if op == "Do": if is_layer_allowed and args: xobj_alias = str(args[-1]).lstrip("/") if xobj_alias in image_map and image_map[xobj_alias]: img_info = image_map[xobj_alias].popleft() cs_name = str(img_info[5]).upper() img_cmyk = None img_spot = None if "CMYK" in cs_name: img_cmyk = (100, 100, 100, 100) elif "GRAY" in cs_name: img_cmyk = (0, 0, 0, 100) elif "RGB" in cs_name: img_cmyk = (100, 100, 100, 100) elif xobj_alias in cs_map: img_spot = cs_map[xobj_alias].get("real_name") img_xref = img_info[0] img_rect = self.page.get_image_bbox(name=img_info[7]) s_mask = img_info[1] el = ParsedDrawingElement( index=-1, drawing={}, layer_name=cur_layer, fill_cmyk=img_cmyk, fill_spot_name=img_spot, fill_spot_tint=1.0, is_shading=False, is_image=True, ) setattr(el, "rect", img_rect) setattr(el, "xref", img_xref) setattr(el, "mask_xref", s_mask) yield el args.clear() current_path_items.clear() current_point = None elif op == "sh": if is_layer_allowed and args: sh_alias = str(args[-1]).lstrip("/") sh_colors = self._parse_shading_colors(sh_alias) sh_xref = self._get_shading_xref(sh_alias) if current_path_items: sh_items = list(current_path_items) elif last_clip_items: sh_items = list(last_clip_items) else: sh_items = [("re", self.page.rect)] sh_rect = self.page.rect if sh_items: x0 = min( (item[1].x0 if item[0] == 're' else min(p.x for p in item[1:])) for item in sh_items ) y0 = min( (item[1].y0 if item[0] == 're' else min(p.y for p in item[1:])) for item in sh_items ) x1 = max( (item[1].x1 if item[0] == 're' else max(p.x for p in item[1:])) for item in sh_items ) y1 = max( (item[1].y1 if item[0] == 're' else max(p.y for p in item[1:])) for item in sh_items ) sh_rect = Rect(x0, y0, x1, y1) dummy_geom = {"type": "sh", "items": sh_items, "rect": sh_rect} el = ParsedDrawingElement( index=-1, drawing=dummy_geom, layer_name=cur_layer, is_shading=True, shading_name=sh_alias, shading_colors=sh_colors, ) setattr(el, "shading_xref", sh_xref) setattr(el, "rect", sh_rect) setattr(el, "is_pattern_shading", False) yield el current_path_items.clear() last_clip_items.clear() current_point = None args.clear() elif op in _DRAWING_OPS: if is_layer_allowed: if drawing_idx < total_drawings: geom = drawings[drawing_idx] geom_type = geom.get("type", "") has_fill = "f" in geom_type or geom_type == "fs" has_stroke = "s" in geom_type or geom_type == "fs" resolved_layer = geom.get("layer") or cur_layer f_cmyk = cur_fill_cmyk if (has_fill and not cur_fill_spot) else None if has_fill and not cur_fill_spot and f_cmyk is None: f_cmyk = self._extract_cmyk_from_geom(geom.get("fill")) s_cmyk = cur_stroke_cmyk if (has_stroke and not cur_stroke_spot) else None if has_stroke and not cur_stroke_spot and s_cmyk is None: s_cmyk = self._extract_cmyk_from_geom(geom.get("color")) yield ParsedDrawingElement( index=drawing_idx, drawing=geom, layer_name=resolved_layer, fill_cmyk=f_cmyk, fill_spot_name=cur_fill_spot if has_fill else None, fill_spot_tint=cur_fill_tint if has_fill else 0.0, stroke_cmyk=s_cmyk, stroke_spot_name=cur_stroke_spot if has_stroke else None, stroke_spot_tint=cur_stroke_tint if has_stroke else 0.0, is_shading=False, ) drawing_idx += 1 current_path_items.clear() current_point = None args.clear() elif op == "n": current_path_items.clear() current_point = None args.clear() elif op in ("cs", "CS") or op in _COLOR_OPS: num_args = [a for a in args if isinstance(a, (int, float))] if op in ("cs", "CS"): if args: cs_alias = str(args[-1]).lstrip("/") if cs_alias in cs_map: spot_value = cs_map[cs_alias].get("real_name") elif "PANTONE" in cs_alias.upper(): spot_value = cs_alias.replace("_", " ") else: spot_value = None if op == "cs": cur_fill_spot = spot_value else: cur_stroke_spot = spot_value args.clear() elif op in ("rg", "RG") and len(num_args) >= 3: cmyk_val = rgb_to_cmyk(num_args[-3], num_args[-2], num_args[-1]) if op == "rg": cur_fill_cmyk = cmyk_val cur_fill_spot = None cur_fill_tint = 1.0 else: cur_stroke_cmyk = cmyk_val cur_stroke_spot = None cur_stroke_tint = 1.0 args.clear() elif op == "k" and len(num_args) >= 4: cur_fill_cmyk = ( int(round(num_args[-4] * 100)), int(round(num_args[-3] * 100)), int(round(num_args[-2] * 100)), int(round(num_args[-1] * 100)), ) cur_fill_spot = None cur_fill_tint = 1.0 args.clear() elif op == "K" and len(num_args) >= 4: cur_stroke_cmyk = ( int(round(num_args[-4] * 100)), int(round(num_args[-3] * 100)), int(round(num_args[-2] * 100)), int(round(num_args[-1] * 100)), ) cur_stroke_spot = None cur_stroke_tint = 1.0 args.clear() elif op in ("sc", "scn"): if len(num_args) >= 4: cur_fill_cmyk = ( int(round(num_args[-4] * 100)), int(round(num_args[-3] * 100)), int(round(num_args[-2] * 100)), int(round(num_args[-1] * 100)), ) cur_fill_spot = None cur_fill_tint = 1.0 elif cur_fill_spot is not None and len(num_args) >= 1: cur_fill_tint = float(num_args[-1]) elif len(num_args) == 1: gray_val = float(num_args[-1]) cur_fill_cmyk = (0, 0, 0, int(round((1.0 - gray_val) * 100))) cur_fill_spot = None args.clear() elif op in ("SC", "SCN"): if len(num_args) >= 4: cur_stroke_cmyk = ( int(round(num_args[-4] * 100)), int(round(num_args[-3] * 100)), int(round(num_args[-2] * 100)), int(round(num_args[-1] * 100)), ) cur_stroke_spot = None cur_stroke_tint = 1.0 elif cur_stroke_spot is not None and len(num_args) >= 1: cur_stroke_tint = float(num_args[-1]) elif len(num_args) == 1: gray_val = float(num_args[-1]) cur_stroke_cmyk = (0, 0, 0, int(round((1.0 - gray_val) * 100))) cur_stroke_spot = None args.clear() elif op in ("g", "G"): if len(num_args) >= 1: gray_val = float(num_args[-1]) k_val = int(round((1.0 - gray_val) * 100)) cmyk_tuple = (0, 0, 0, k_val) if op == "g": cur_fill_cmyk = cmyk_tuple cur_fill_spot = None cur_fill_tint = 1.0 else: cur_stroke_cmyk = cmyk_tuple cur_stroke_spot = None cur_stroke_tint = 1.0 args.clear() elif op == "BDC": layer_stack.append((cur_layer, is_layer_allowed)) if len(args) >= 2 and args[-2] == "/OC": current_layer_alias = args[-1].lstrip("/") resolved_real_layer = self.layer_map.get( current_layer_alias, current_layer_alias ) is_layer_allowed = self.is_layer_allowed_for_page( resolved_real_layer, allowed_layers ) cur_layer = resolved_real_layer args.clear() elif op == "EMC": if layer_stack: cur_layer, is_layer_allowed = layer_stack.pop() else: cur_layer = "Root" is_layer_allowed = True args.clear() else: args.clear()