import mammoth from io import BytesIO from bs4 import BeautifulSoup from docx import Document from docx.shared import Pt, Inches from docx.enum.text import WD_ALIGN_PARAGRAPH from app.services.file_storage import get_file_content ALIGN_MAP = { WD_ALIGN_PARAGRAPH.CENTER: "center", WD_ALIGN_PARAGRAPH.RIGHT: "right", WD_ALIGN_PARAGRAPH.JUSTIFY: "justify", } async def docx_to_html(file_content: bytes) -> str: result = mammoth.convert_to_html(BytesIO(file_content)) html = result.value try: doc = Document(BytesIO(file_content)) soup = BeautifulSoup(html, "html.parser") paragraphs = doc.paragraphs html_blocks = soup.find_all(["p", "h1", "h2", "h3", "h4", "h5", "h6", "li"]) for i, para in enumerate(paragraphs): if i >= len(html_blocks): break if para.alignment and para.alignment in ALIGN_MAP: css = ALIGN_MAP[para.alignment] existing = html_blocks[i].get("style", "") styles = f"text-align:{css}" if existing: styles = existing.rstrip(";") + ";" + styles html_blocks[i]["style"] = styles html = str(soup) except Exception: pass return html def html_to_docx_bytes(html_content: str) -> bytes: from html.parser import HTMLParser doc = Document() style = doc.styles["Normal"] font = style.font font.name = "Arial" font.size = Pt(11) class RichHTMLParser(HTMLParser): def __init__(self): super().__init__() self.paragraphs: list[dict] = [] self.current = {"runs": [], "align": None} self.in_paragraph = False self.current_run = {"text": "", "bold": False, "italic": False, "underline": False} self.tag_stack: list[str] = [] self.heading_level = 0 def handle_starttag(self, tag, attrs): tag_lower = tag.lower() attrs_dict = dict(attrs) if tag_lower in ("p", "div", "li"): self.in_paragraph = True self.current = {"runs": [], "align": None} self.current_run = {"text": "", "bold": False, "italic": False, "underline": False} style = attrs_dict.get("style", "") if "text-align:center" in style: self.current["align"] = "center" elif "text-align:right" in style: self.current["align"] = "right" elif "text-align:justify" in style: self.current["align"] = "justify" elif tag_lower in ("h1", "h2", "h3", "h4", "h5", "h6"): self.in_paragraph = True self.heading_level = int(tag_lower[1]) self.current_run = {"text": "", "bold": False, "italic": False, "underline": False} elif tag_lower in ("strong", "b"): self.current_run["bold"] = True elif tag_lower in ("em", "i"): self.current_run["italic"] = True elif tag_lower == "u": self.current_run["underline"] = True elif tag_lower in ("br",): if self.in_paragraph: self.current["runs"].append(dict(self.current_run)) self.current_run = {"text": "", "bold": False, "italic": False, "underline": False} self.tag_stack.append(tag_lower) def handle_endtag(self, tag): tag_lower = tag.lower() if tag_lower in ("p", "div", "li", "h1", "h2", "h3", "h4", "h5", "h6"): if self.current_run["text"].strip(): self.current["runs"].append(dict(self.current_run)) if self.current["runs"]: p = dict(self.current) p["heading"] = self.heading_level self.paragraphs.append(p) self.current = {"runs": []} self.current_run = {"text": "", "bold": False, "italic": False, "underline": False} self.in_paragraph = False self.heading_level = 0 if self.tag_stack: self.tag_stack.pop() def handle_data(self, data): if self.in_paragraph: self.current_run["text"] += data parser = RichHTMLParser() parser.feed(html_content) for para_data in parser.paragraphs: heading = para_data.get("heading", 0) if heading > 0: p = doc.add_heading(level=min(heading, 9)) else: p = doc.add_paragraph() align_val = para_data.get("align") if align_val == "center": p.alignment = WD_ALIGN_PARAGRAPH.CENTER elif align_val == "right": p.alignment = WD_ALIGN_PARAGRAPH.RIGHT elif align_val == "justify": p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY for run_data in para_data.get("runs", []): run = p.add_run(run_data["text"]) if run_data.get("bold"): run.bold = True if run_data.get("italic"): run.italic = True if run_data.get("underline"): run.underline = True output = BytesIO() doc.save(output) return output.getvalue() def docx_to_pdf_bytes(file_content: bytes) -> bytes: doc = Document(BytesIO(file_content)) from io import BytesIO as Bio from reportlab.lib.pagesizes import A4 from reportlab.lib.styles import getSampleStyleSheet, ParagraphStyle from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer from reportlab.lib.enums import TA_LEFT buffer = Bio() pdf_doc = SimpleDocTemplate(buffer, pagesize=A4) styles = getSampleStyleSheet() story = [] for para in doc.paragraphs: if para.text.strip(): p = Paragraph(para.text, styles["Normal"]) story.append(p) story.append(Spacer(1, 6)) pdf_doc.build(story) return buffer.getvalue()