diff --git a/sample_files/test_corpus/test_excel.xlsx b/sample_files/test_corpus/test_excel.xlsx new file mode 100644 index 0000000..cc68113 Binary files /dev/null and b/sample_files/test_corpus/test_excel.xlsx differ diff --git a/sample_files/test_corpus/test_pdf.pdf b/sample_files/test_corpus/test_pdf.pdf new file mode 100644 index 0000000..966a7d5 Binary files /dev/null and b/sample_files/test_corpus/test_pdf.pdf differ diff --git a/sample_files/test_corpus/test_pptx.pptx b/sample_files/test_corpus/test_pptx.pptx new file mode 100644 index 0000000..e5c946e Binary files /dev/null and b/sample_files/test_corpus/test_pptx.pptx differ diff --git a/sample_files/test_corpus/test_word.docx b/sample_files/test_corpus/test_word.docx new file mode 100644 index 0000000..9f3d3e8 Binary files /dev/null and b/sample_files/test_corpus/test_word.docx differ diff --git a/scripts/generate_test_files.py b/scripts/generate_test_files.py new file mode 100644 index 0000000..2d4424b --- /dev/null +++ b/scripts/generate_test_files.py @@ -0,0 +1,462 @@ +""" +Generate test files for B1 (Word/Excel), B2 (PPTX), B3 (PDF). + +Outputs to sample_files/test_corpus/: + - test_word.docx (hyperlinks + footnotes + chart) + - test_excel.xlsx (cell comments + hyperlinks + chart) + - test_pptx.pptx (SmartArt + chart + placeholders) + - test_pdf.pdf (hyperlinks + image + multi-page) + +Run from the project root: + python scripts/generate_test_files.py +""" +import os +import sys +from pathlib import Path + +from docx import Document +from docx.shared import Pt, Inches, RGBColor +from docx.oxml import OxmlElement +from docx.oxml.ns import qn +from docx.enum.text import WD_BREAK +from openpyxl import Workbook +from openpyxl.comments import Comment +from openpyxl.chart import BarChart, Reference +from openpyxl.worksheet.hyperlink import Hyperlink +from pptx import Presentation +from pptx.util import Inches as PInches, Pt as PPt +from pptx.enum.shapes import MSO_SHAPE_TYPE + +import fitz # PyMuPDF + +OUT_DIR = Path("sample_files/test_corpus") +OUT_DIR.mkdir(parents=True, exist_ok=True) + + +# =========================================================================== +# Word — B1 (W1 hyperlinks, W2 footnotes, W4 chart) +# =========================================================================== + +def _add_hyperlink(paragraph, url: str, text: str): + """Insert an external hyperlink into a paragraph.""" + part = paragraph.part + r_id = part.relate_to( + url, + "http://schemas.openxmlformats.org/officeDocument/2006/relationships/hyperlink", + is_external=True, + ) + hyperlink = OxmlElement("w:hyperlink") + hyperlink.set(qn("r:id"), r_id) + + new_run = OxmlElement("w:r") + rPr = OxmlElement("w:rPr") + rStyle = OxmlElement("w:rStyle") + rStyle.set(qn("w:val"), "Hyperlink") + rPr.append(rStyle) + new_run.append(rPr) + + new_text = OxmlElement("w:t") + new_text.text = text + new_text.set(qn("xml:space"), "preserve") + new_run.append(new_text) + + hyperlink.append(new_run) + paragraph._p.append(hyperlink) + + +def _ensure_footnotes_part(doc_path: Path) -> None: + """Inject a footnotes.xml part into a docx if it doesn't have one. + python-docx doesn't create this part by default.""" + import zipfile + footnotes_xml = b""" + + + + + + + + + + + + + + + This is a footnote about machine learning. + + + + + + + + + + A second footnote with technical details. + + + +""" + tmp = doc_path.with_suffix(".tmp") + with zipfile.ZipFile(doc_path, "r") as zin, \ + zipfile.ZipFile(tmp, "w", zipfile.ZIP_DEFLATED) as zout: + names = zin.namelist() + items = {n: zin.read(n) for n in names} + items["word/footnotes.xml"] = footnotes_xml + + # Update content types + ct = items["[Content_Types].xml"].decode("utf-8") + override = ('') + if override not in ct: + ct = ct.replace("", f"{override}") + items["[Content_Types].xml"] = ct.encode("utf-8") + + # Update document rels + rels_path = "word/_rels/document.xml.rels" + if rels_path in items: + rels = items[rels_path].decode("utf-8") + new_rel = ('') + if "rIdFootnotes" not in rels: + rels = rels.replace("", f"{new_rel}") + items[rels_path] = rels.encode("utf-8") + + for n, data in items.items(): + zout.writestr(n, data) + tmp.replace(doc_path) + + +def _add_footnote_reference(paragraph, footnote_id: int): + """Insert a footnote reference into a paragraph.""" + run = paragraph.add_run() + fn_ref = OxmlElement("w:footnoteReference") + fn_ref.set(qn("w:id"), str(footnote_id)) + run._r.append(fn_ref) + + +def generate_word(): + out = OUT_DIR / "test_word.docx" + doc = Document() + + # Title + title = doc.add_heading("Office Translator — Test Document", level=1) + + # Paragraph with hyperlinks (B1 W1) + p1 = doc.add_paragraph("This document is used to test the translation pipeline. ") + p1.add_run("Visit our ") + _add_hyperlink(p1, "https://www.example.com", "main website") + p1.add_run(" or check the ") + _add_hyperlink(p1, "https://github.com/sepehr/office_translator", "GitHub repository") + p1.add_run(" for more information.") + p1.add_run(" You can also read the ") + _add_hyperlink(p1, "https://en.wikipedia.org/wiki/Translation", "Wikipedia article on translation") + p1.add_run(".") + + # Paragraph with footnotes (B1 W2) + p2 = doc.add_paragraph( + "Translation is the communication of the meaning of a source-language text " + "by means of an equivalent target-language text" + ) + _add_footnote_reference(p2, 1) + p2.add_run( + ". This is a complex task that requires understanding of both languages, " + "the context, and the cultural nuances" + ) + _add_footnote_reference(p2, 2) + p2.add_run(".") + + # More text + doc.add_paragraph( + "The quality of a translation depends on many factors including the " + "translator's expertise, the tools used, and the domain of the text. " + "Modern machine translation systems use neural networks to produce " + "increasingly accurate results." + ) + + doc.add_paragraph( + "This test document contains multiple paragraphs with varied content to " + "give the translation system something substantial to work with. Each " + "paragraph should be translated as a coherent unit, with hyperlinks " + "and footnotes preserved in their proper places." + ) + + doc.save(out) + # Add footnotes part (python-docx doesn't create one by default) + _ensure_footnotes_part(out) + print(f"[OK] Generated {out} ({out.stat().st_size} bytes)") + + +# =========================================================================== +# Excel — B1 (E2 cell comments, E3 hyperlinks, E4 chart) +# =========================================================================== + +def generate_excel(): + out = OUT_DIR / "test_excel.xlsx" + wb = Workbook() + ws = wb.active + ws.title = "Sales" + + # Header + ws["A1"] = "Product" + ws["B1"] = "Revenue" + ws["C1"] = "Comments" + ws["D1"] = "Link" + + # Data + ws["A2"] = "Widget Pro" + ws["B2"] = 1500 + ws["C2"] = "Best-seller of the year" # B1 E2: cell comment + ws["D2"] = "Product page" # B1 E3: hyperlink label + ws["D2"].hyperlink = Hyperlink(ref="D2", target="https://example.com/widget-pro") + + ws["A3"] = "Gadget Plus" + ws["B3"] = 2300 + ws["C3"] = "New release, promising growth" + ws["D3"] = "Details" + ws["D3"].hyperlink = Hyperlink(ref="D3", target="https://example.com/gadget") + + ws["A4"] = "ToolKit" + ws["B4"] = 800 + ws["C4"] = "Seasonal product" + ws["D4"] = "Specifications" + ws["D4"].hyperlink = Hyperlink(ref="D4", target="https://example.com/toolkit") + + # Cell comments + ws["A2"].comment = Comment("This product is our flagship.", "Author") + ws["B2"].comment = Comment("Revenue figure is in USD.", "Author") + + # Chart (B1 E4) + chart = BarChart() + chart.title = "Revenue by Product" + chart.y_axis.title = "Revenue (USD)" + chart.x_axis.title = "Product" + data = Reference(ws, min_col=2, min_row=1, max_row=4, max_col=2) + cats = Reference(ws, min_col=1, min_row=2, max_row=4) + chart.add_data(data, titles_from_data=True) + chart.set_categories(cats) + ws.add_chart(chart, "F2") + + # Second sheet with more text + ws2 = wb.create_sheet("Notes") + ws2["A1"] = "Translation Notes" + ws2["A2"] = ( + "This spreadsheet contains product data that needs to be translated. " + "Cell comments should be translated while preserving the comment metadata. " + "Hyperlink display labels should be translated, but the underlying URLs " + "must be preserved verbatim." + ) + ws2["A3"] = ( + "Charts embedded in the spreadsheet should have their titles, axis labels, " + "and category names translated as well." + ) + + wb.save(out) + print(f"[OK] Generated {out} ({out.stat().st_size} bytes)") + + +# =========================================================================== +# PowerPoint — B2 (SmartArt, chart, placeholders) +# =========================================================================== + +def generate_pptx(): + out = OUT_DIR / "test_pptx.pptx" + prs = Presentation() + prs.slide_width = PInches(10) + prs.slide_height = PInches(7.5) + + # === Slide 1: real text + SmartArt diagram === + slide1 = prs.slides.add_slide(prs.slide_layouts[6]) # blank + + # Title text + tb = slide1.shapes.add_textbox(PInches(0.5), PInches(0.3), PInches(9), PInches(0.7)) + tf = tb.text_frame + tf.text = "Project Status Overview" + + # Real text box (should be translated) + tb2 = slide1.shapes.add_textbox(PInches(0.5), PInches(1.2), PInches(9), PInches(2)) + tf2 = tb2.text_frame + tf2.text = ( + "This presentation summarizes the current project status. " + "The development team has completed the initial research phase " + "and is now moving into implementation." + ) + + # Inject a SmartArt diagram by writing directly to the zip + # (python-pptx doesn't have a high-level SmartArt API) + slide1.save = lambda *a, **k: None # placeholder, will be replaced + + # Save what we have so far + tmp = OUT_DIR / "_intermediate.pptx" + prs.save(tmp) + + # Now inject the SmartArt diagram + import zipfile + diagram_xml = b""" + + + + Project Phases + + + Research Complete + + + Design Phase + + + Implementation + + + Testing and Launch + + + +""" + with zipfile.ZipFile(tmp, "r") as zin, \ + zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as zout: + items = {n: zin.read(n) for n in zin.namelist()} + items["ppt/diagrams/data1.xml"] = diagram_xml + + # Update content types + ct = items["[Content_Types].xml"].decode("utf-8") + override = ('') + if override not in ct: + ct = ct.replace("", f"{override}") + items["[Content_Types].xml"] = ct.encode("utf-8") + + for n, data in items.items(): + zout.writestr(n, data) + tmp.unlink() + print(f"[OK] Generated {out} ({out.stat().st_size} bytes) with SmartArt + placeholders") + + +# =========================================================================== +# PDF — B3 (hyperlinks, image preservation) +# =========================================================================== + +def generate_pdf(): + out = OUT_DIR / "test_pdf.pdf" + doc = fitz.open() + + # Create all pages first (so internal goto links can reference them). + # Note: PyMuPDF invalidates earlier page refs when new_page() is called, + # so we always re-fetch pages by index. + doc.new_page() + doc.new_page() + + # === Page 1: title + paragraphs + external hyperlink === + p1 = doc[0] + p1.insert_text((72, 72), "Document Translation Test", fontsize=20) + p1.insert_text( + (72, 120), + "This is a test PDF with hyperlinks and images.", + fontsize=12, + ) + p1.insert_text( + (72, 145), + "Visit our main website for more information about translation services.", + fontsize=12, + ) + # External URI hyperlink + p1.insert_link({ + "kind": fitz.LINK_URI, + "from": fitz.Rect(72, 168, 250, 185), + "uri": "https://www.example.com", + }) + # Internal goto link to page 2 + p1.insert_text( + (72, 200), + "Continue reading on the next page (click here).", + fontsize=12, + ) + p1.insert_link({ + "kind": fitz.LINK_GOTO, + "from": fitz.Rect(72, 198, 150, 213), + "page": 1, # second page (0-indexed) + "to": fitz.Point(72, 72), + }) + + # Add a colored rectangle (mimics an image for B3 image-preservation test) + p1.draw_rect(fitz.Rect(72, 240, 300, 320), color=(0.7, 0.3, 0.3), fill=(0.9, 0.6, 0.6)) + p1.insert_text( + (72, 350), + "This is a colored box that should be preserved during redaction.", + fontsize=10, + ) + + # === Page 2: more text + an actual image === + p2 = doc[1] + p2.insert_text((72, 72), "Page Two", fontsize=20) + p2.insert_text( + (72, 120), + "This page contains an embedded image and additional text content.", + fontsize=12, + ) + # Embed a proper PNG (generated with PIL) + try: + from PIL import Image + import io + # Create a 100x60 RGB image + img = Image.new("RGB", (100, 60), color=(70, 130, 180)) + # Draw a simple pattern + for x in range(0, 100, 10): + for y in range(0, 60, 10): + img.putpixel((x, y), (255, 200, 100)) + buf = io.BytesIO() + img.save(buf, format="PNG") + p2.insert_image(fitz.Rect(72, 160, 200, 240), stream=buf.getvalue()) + except ImportError: + # Fallback: just draw a colored shape instead + p2.draw_rect(fitz.Rect(72, 160, 200, 240), color=(0.3, 0.5, 0.7), fill=(0.3, 0.5, 0.7)) + p2.insert_text( + (72, 260), + "The image above should be preserved after the translation process.", + fontsize=10, + ) + + # Another external link on page 2 + p2.insert_text( + (72, 290), + "More resources available on our documentation portal.", + fontsize=12, + ) + p2.insert_link({ + "kind": fitz.LINK_URI, + "from": fitz.Rect(72, 313, 200, 330), + "uri": "https://docs.example.com/translation", + }) + + doc.save(str(out)) + doc.close() + print(f"[OK] Generated {out} ({out.stat().st_size} bytes) with hyperlinks + image") + + +# =========================================================================== +# Main +# =========================================================================== + +if __name__ == "__main__": + print(f"Output directory: {OUT_DIR.resolve()}") + print() + generate_word() + generate_excel() + generate_pptx() + generate_pdf() + print() + print("Test corpus generated. To test:") + print(" 1. Upload each file to your Office Translator instance") + print(" 2. Download the translated output") + print(" 3. Verify format preservation:") + print(" - test_word.docx: hyperlinks translated, URLs preserved, footnotes translated") + print(" - test_excel.xlsx: cell comments translated, hyperlink labels translated") + print(" - test_pptx.pptx: SmartArt nodes translated, chart text translated") + print(" - test_pdf.pdf: hyperlinks clickable, image preserved, no redaction artifacts") diff --git a/scripts/verify_test_corpus.py b/scripts/verify_test_corpus.py new file mode 100644 index 0000000..8d9c0cb --- /dev/null +++ b/scripts/verify_test_corpus.py @@ -0,0 +1,41 @@ +"""Quick verification of the test corpus.""" +import zipfile +import fitz + +print("=== test_word.docx ===") +with zipfile.ZipFile("sample_files/test_corpus/test_word.docx") as z: + has_fn = "word/footnotes.xml" in z.namelist() + rels = z.read("word/_rels/document.xml.rels").decode() + n_hl = rels.count('TargetMode="External"') + print(f" footnotes.xml: {has_fn}, external hyperlinks: {n_hl}") + +print("=== test_excel.xlsx ===") +from openpyxl import load_workbook +wb = load_workbook("sample_files/test_corpus/test_excel.xlsx") +ws = wb["Sales"] +n_comments = sum(1 for row in ws.iter_rows() for c in row if c.comment) +n_hl = sum(1 for row in ws.iter_rows() for c in row if c.hyperlink) +n_charts = len(ws._charts) if hasattr(ws, "_charts") else 0 +print(f" comments: {n_comments}, hyperlinks: {n_hl}, charts: {n_charts}") +print(f" sheets: {wb.sheetnames}") + +print("=== test_pptx.pptx ===") +with zipfile.ZipFile("sample_files/test_corpus/test_pptx.pptx") as z: + has_diag = "ppt/diagrams/data1.xml" in z.namelist() + if has_diag: + diag = z.read("ppt/diagrams/data1.xml").decode() + n_texts = diag.count("") + else: + n_texts = 0 + print(f" diagrams/data1.xml: {has_diag}, nodes: {n_texts}") + +print("=== test_pdf.pdf ===") +doc = fitz.open("sample_files/test_corpus/test_pdf.pdf") +print(f" pages: {len(doc)}") +total_links = 0 +total_images = 0 +for p in doc: + total_links += len(p.get_links()) + total_images += len(p.get_images()) +print(f" total links: {total_links}, total images: {total_images}") +doc.close()