169 lines
5.9 KiB
Python
169 lines
5.9 KiB
Python
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_paras = soup.find_all("p")
|
|
|
|
for i, para in enumerate(paragraphs):
|
|
if i >= len(html_paras):
|
|
break
|
|
if para.alignment and para.alignment in ALIGN_MAP:
|
|
css = ALIGN_MAP[para.alignment]
|
|
existing_style = html_paras[i].get("style", "")
|
|
styles = f"text-align:{css}"
|
|
if existing_style:
|
|
styles = existing_style.rstrip(";") + ";" + styles
|
|
html_paras[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()
|