Source code for sbi_special_docx_master.special_docx

import base64
import os
from io import BytesIO
import copy
from typing import Dict, List, Optional, Any, TypeAlias

from docx.document import Document as DocxDocument
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.shared import Inches
from PIL import Image

from .logger import logger
from .validator import validate_data_pydantic
from .styles import DocEditorEmpty

# Set the default image height in inches, configurable via environment variable.
SBIDOCX_PICHEIGHT = float(os.environ.get("SBIDOCX_PICHEIGHT", 2.5))
# Set the block name for the document section, configurable via environment variable.
SBIDOCX_BLOCKNAME = os.getenv("SBIDOCX_BLOCKNAME", "ІНФОРМАЦІЯ ОТ")

InputDataAlias: TypeAlias = Dict[str, List[Dict[str, Any]]]


[docs]class AddDocx: """ Class for working with specific blocks in a document. It adds textual and image information to a Document object. Ensures that the original document is returned if an error occurs during modifications. :ivar _orig_doc: DocxDocument - The original document. :ivar info: dict - A dictionary containing information to be added. :ivar _new_doc: Optional[DocxDocument] - The new document after modifications (if successful). """ def __init__(self, doc: DocxDocument, info: InputDataAlias) -> None: """ Initializes the AddDocx object. :param doc: The document object to which information will be added. :param info: A dictionary containing the information for addition. :raises TypeError: If 'doc' is not an instance of DocxDocument. :raises ValueError: If 'info' is not a dictionary. """ if not isinstance(doc, DocxDocument): error = f"Received document of type {type(doc)}, expected docx.Document." logger.critical(error) raise TypeError(error) if not isinstance(info, dict): logger.critical("No data provided for adding to the document or the data format is incorrect.") raise ValueError("Invalid input data format.") self._orig_doc: DocxDocument = doc # Store the original document as a private variable. self.info: InputDataAlias = info # Information provided as a dictionary. self._new_doc = None # To store the new document after modifications. # Validate the input data using the pydantic validator. validated_items = validate_data_pydantic(self.info) try: self._new_doc = self._add_info(validated_items) except Exception as e: logger.error(f"Error {e} while adding information block") @property def document(self) -> DocxDocument: """ Returns the document instance with the required modifications, or the original document if the modified version could not be generated. """ return self._new_doc if self._new_doc is not None else self._orig_doc @property def has_succeeded(self) -> bool: """ Returns True if the modified document was successfully created, or False if the document could not be generated. """ return self._new_doc is not None
[docs] def _add_info(self, info: List): """ Adds textual and image information to the document. :param info: A list of dictionaries containing titles, content, and images. """ # If there is no information to add, exit the function. if not info: return # Clone the original document to attempt modifications without affecting the original. doc = copy.deepcopy(self._orig_doc) doc = DocEditorEmpty(doc).document # Add the block header using the configured block name. doc.add_paragraph(SBIDOCX_BLOCKNAME, style="left_header") for item in info: # Add a paragraph for the title with a bullet style. title_paragraph = doc.add_paragraph(style='text_base') run = title_paragraph.add_run(item['title']) run.bold = True # Make the title bold content = item['content'] # Check if content is a list of paragraphs. if isinstance(content, list): for entry in content: doc.add_paragraph(text=entry, style='text_base') else: # If content is not a list, add it directly. doc.add_paragraph(content, style='text_base') doc.add_paragraph() # Add an empty paragraph for spacing # Attempt to add images from the provided items. try: for photo in item['images']: # Assumes that each photo dict has a 'file' key containing a Base64 string. doc = self._add_image_from_base64(photo['file'], SBIDOCX_PICHEIGHT, doc) except Exception as e: logger.info(f"Provided file is not a photo: {e}") return doc
[docs] @staticmethod def _add_image_from_base64(base64_string: str, height: float, doc: DocxDocument): """ Adds an image from a Base64 string to the document. :param base64_string: The image string in Base64 format. :param height: The height of the image in inches. """ try: # Decode the Base64 string into bytes. image_bytes = base64.b64decode(base64_string) image_stream = BytesIO(image_bytes) # Example code for image processing: try: image = Image.open(image_stream) # Check image integrity. image.verify() # After calling verify(), reset the file pointer and reopen the image. image_stream.seek(0) image = Image.open(image_stream) # Convert the image into a supported format if needed. supported_formats = ["JPEG", "PNG"] if image.format not in supported_formats: converted_stream = BytesIO() image.save(converted_stream, format="JPEG") converted_stream.seek(0) image_stream = converted_stream except Exception as e: logger.warning(f"Error {e} processing image with Pillow") # Add the image to the document and center it. paragraph = doc.add_paragraph(style='text_base') paragraph.alignment = WD_ALIGN_PARAGRAPH.CENTER run = paragraph.add_run() run.add_picture(image_stream, height=Inches(height)) doc.add_paragraph() # Add spacing after the image. logger.info("Image for the separate information added successfully.") except Exception as e: logger.warning(f"Failed to add image: {e}") logger.warning(f"Length of Base64 string: {len(base64_string)}") return doc
[docs] def save_io(self) -> Optional[BytesIO]: """ Saves the document to an in-memory stream (BytesIO). :return: A BytesIO stream with the saved document or None if an error occurs. """ try: if self._new_doc: saving_doc = self._new_doc else: saving_doc = self._orig_doc output_stream = BytesIO() saving_doc.save(output_stream) output_stream.seek(0) logger.info("Document saving completed successfully.") return output_stream except Exception as e: logger.error(f"Error saving the document: {e}") return None
[docs] def save(self, filename: str): """ Saves the document to a file on disk. :param filename: The path to the file where the document will be saved. """ try: if self._new_doc: self._new_doc.save(filename) logger.info(f"Document successfully saved as {filename}") else: self._orig_doc.save(filename) logger.info(f"Document successfully saved as {filename}") except Exception as e: logger.error(f"Error saving the document: {e}")