diff --git a/Dockerfile b/Dockerfile index 39f15b9..546440c 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,7 @@ FROM pandoc/typst:3.7.0 ARG ENVIRONMENT=production RUN apk add --no-cache python3 py3-pip +RUN apk add --no-cache poppler-utils RUN apk add --no-cache font-noto-cjk font-noto-all font-noto-emoji ttf-dejavu fontconfig \ && fc-cache -f diff --git a/app/main.py b/app/main.py index 3256dca..6ef1d68 100644 --- a/app/main.py +++ b/app/main.py @@ -1,4 +1,4 @@ -from typing import Literal +from typing import Literal, cast import tempfile import asyncio @@ -26,8 +26,9 @@ } class DocConversionTask: - def __init__(self, doc_type: Literal['docx', 'pdf', 'tex'], temp_dir_name: str, env: dict[str, str] | None): + def __init__(self, doc_type: Literal['docx', 'pdf', 'tex'], paper_size: Literal['letter', 'a4'], temp_dir_name: str, env: dict[str, str] | None): self.doc_type = doc_type + self.paper_size: Literal['letter', 'a4'] = paper_size self.temp_dir_name = temp_dir_name self.env = env self.future = asyncio.get_running_loop().create_future() @@ -38,12 +39,26 @@ async def convert(self): match self.doc_type: case "pdf": + paper_size_variable = "us-letter" if self.paper_size == "letter" else "a4" cmd = f"pandoc --from markdown --to pdf --standalone --embed-resources --no-highlight \ - --pdf-engine=typst {MD_FILE_NAME} -o {output_file_name}" + --pdf-engine=typst {MD_FILE_NAME} \ + -V papersize={paper_size_variable} \ + -o {output_file_name}" case "tex": - cmd = f"pandoc --from markdown --to latex --standalone --embed-resources --no-highlight {MD_FILE_NAME} -o {output_file_name}" + cmd = f"pandoc --from markdown --to latex --standalone --embed-resources --no-highlight \ + -V papersize={self.paper_size} \ + {MD_FILE_NAME} -o {output_file_name}" + case "docx": + if self.paper_size == "letter": + reference_doc = "/code/app/reference_docs/reference_letter.docx" + else: + reference_doc = "/code/app/reference_docs/reference_a4.docx" + + cmd = f"pandoc --from markdown --to {self.doc_type} --standalone --embed-resources --no-highlight \ + --reference-doc={reference_doc} \ + {MD_FILE_NAME} -o {output_file_name}" case _: - cmd = f"pandoc --from markdown --to {self.doc_type} --standalone --embed-resources --no-highlight {MD_FILE_NAME} -o {output_file_name}" + raise HTTPException(status_code=405, detail="Unsupported document type") try: if (time.monotonic() - self.wait_start_time) > MAX_QUEUE_WAIT_TIME: @@ -122,9 +137,16 @@ async def delete_temp_directory(temp_dir_name): await asyncio.to_thread(shutil.rmtree, temp_dir_name, ignore_errors=True) -@app.post("/docgen/{doc_type}") -async def convert_markdown_file(request_file: UploadFile, doc_type: Literal['docx', 'pdf', 'tex'], +@app.post("/docgen/{raw_doc_type}") +async def convert_markdown_file(request_file: UploadFile, raw_doc_type: Literal['docx', 'docx_a4', 'pdf', 'pdf_a4', 'tex', 'tex_a4'], background_tasks: BackgroundTasks): + if raw_doc_type.endswith('_a4'): + paper_size = 'a4' + else: + paper_size = 'letter' + + doc_type = cast(Literal['docx', 'pdf', 'tex'], raw_doc_type.removesuffix('_a4')) + temp_dir_name = None try: @@ -154,7 +176,7 @@ async def convert_markdown_file(request_file: UploadFile, doc_type: Literal['doc md_input_file.write(("Created with [EngineeringPaper.xyz](https://engineeringpaper.xyz)\n\n").encode('utf-8')) # add to task queue - doc_conversion_task = DocConversionTask(doc_type, temp_dir_name, env) + doc_conversion_task = DocConversionTask(doc_type, paper_size, temp_dir_name, env) await queue.put(doc_conversion_task) # finishes when task is inserted into queue output_file_name = await doc_conversion_task.future # finishes when doc conversion is completed or raises diff --git a/app/reference_docs/reference_a4.docx b/app/reference_docs/reference_a4.docx new file mode 100644 index 0000000..522d5be Binary files /dev/null and b/app/reference_docs/reference_a4.docx differ diff --git a/app/reference_docs/reference_letter.docx b/app/reference_docs/reference_letter.docx new file mode 100644 index 0000000..d3c5a31 Binary files /dev/null and b/app/reference_docs/reference_letter.docx differ diff --git a/tests/output_reference.docx b/tests/output_reference.docx index f546bc0..d107457 100644 Binary files a/tests/output_reference.docx and b/tests/output_reference.docx differ diff --git a/tests/output_reference.tex b/tests/output_reference.tex index 6e94991..f285c47 100644 --- a/tests/output_reference.tex +++ b/tests/output_reference.tex @@ -2,6 +2,7 @@ \PassOptionsToPackage{unicode}{hyperref} \PassOptionsToPackage{hyphens}{url} \documentclass[ + letterpaper, ]{article} \usepackage{xcolor} \usepackage{amsmath,amssymb} diff --git a/tests/output_reference_0.docx b/tests/output_reference_0.docx index 33f3b24..62ff929 100644 Binary files a/tests/output_reference_0.docx and b/tests/output_reference_0.docx differ diff --git a/tests/output_reference_1.docx b/tests/output_reference_1.docx index d04292c..2d0a4a9 100644 Binary files a/tests/output_reference_1.docx and b/tests/output_reference_1.docx differ diff --git a/tests/output_reference_10.docx b/tests/output_reference_10.docx index 320890e..eb6cef9 100644 Binary files a/tests/output_reference_10.docx and b/tests/output_reference_10.docx differ diff --git a/tests/output_reference_11.docx b/tests/output_reference_11.docx index fa4f266..d7527bc 100644 Binary files a/tests/output_reference_11.docx and b/tests/output_reference_11.docx differ diff --git a/tests/output_reference_12.docx b/tests/output_reference_12.docx index 77b0d46..43abeec 100644 Binary files a/tests/output_reference_12.docx and b/tests/output_reference_12.docx differ diff --git a/tests/output_reference_13.docx b/tests/output_reference_13.docx index e775535..5655cf1 100644 Binary files a/tests/output_reference_13.docx and b/tests/output_reference_13.docx differ diff --git a/tests/output_reference_14.docx b/tests/output_reference_14.docx index 5f4f437..7d5c9f5 100644 Binary files a/tests/output_reference_14.docx and b/tests/output_reference_14.docx differ diff --git a/tests/output_reference_15.docx b/tests/output_reference_15.docx index b4e929e..efe82f5 100644 Binary files a/tests/output_reference_15.docx and b/tests/output_reference_15.docx differ diff --git a/tests/output_reference_16.docx b/tests/output_reference_16.docx index 5e5522c..1eed0dc 100644 Binary files a/tests/output_reference_16.docx and b/tests/output_reference_16.docx differ diff --git a/tests/output_reference_17.docx b/tests/output_reference_17.docx index b97c7c1..6321d3b 100644 Binary files a/tests/output_reference_17.docx and b/tests/output_reference_17.docx differ diff --git a/tests/output_reference_18.docx b/tests/output_reference_18.docx index 24a1b12..5356212 100644 Binary files a/tests/output_reference_18.docx and b/tests/output_reference_18.docx differ diff --git a/tests/output_reference_19.docx b/tests/output_reference_19.docx index 525a018..de3969b 100644 Binary files a/tests/output_reference_19.docx and b/tests/output_reference_19.docx differ diff --git a/tests/output_reference_2.docx b/tests/output_reference_2.docx index 8e76096..c279cd3 100644 Binary files a/tests/output_reference_2.docx and b/tests/output_reference_2.docx differ diff --git a/tests/output_reference_3.docx b/tests/output_reference_3.docx index 039d576..e37e752 100644 Binary files a/tests/output_reference_3.docx and b/tests/output_reference_3.docx differ diff --git a/tests/output_reference_4.docx b/tests/output_reference_4.docx index d0d66ee..0d5b18f 100644 Binary files a/tests/output_reference_4.docx and b/tests/output_reference_4.docx differ diff --git a/tests/output_reference_5.docx b/tests/output_reference_5.docx index fd08600..386936e 100644 Binary files a/tests/output_reference_5.docx and b/tests/output_reference_5.docx differ diff --git a/tests/output_reference_6.docx b/tests/output_reference_6.docx index 7148ea0..63ee113 100644 Binary files a/tests/output_reference_6.docx and b/tests/output_reference_6.docx differ diff --git a/tests/output_reference_7.docx b/tests/output_reference_7.docx index 92abb5b..f3854c2 100644 Binary files a/tests/output_reference_7.docx and b/tests/output_reference_7.docx differ diff --git a/tests/output_reference_8.docx b/tests/output_reference_8.docx index 964ffa0..69f244b 100644 Binary files a/tests/output_reference_8.docx and b/tests/output_reference_8.docx differ diff --git a/tests/output_reference_9.docx b/tests/output_reference_9.docx index cba8888..9216862 100644 Binary files a/tests/output_reference_9.docx and b/tests/output_reference_9.docx differ diff --git a/tests/output_reference_a4.docx b/tests/output_reference_a4.docx new file mode 100644 index 0000000..7156d2a Binary files /dev/null and b/tests/output_reference_a4.docx differ diff --git a/tests/output_reference_a4.tex b/tests/output_reference_a4.tex new file mode 100644 index 0000000..826cf45 --- /dev/null +++ b/tests/output_reference_a4.tex @@ -0,0 +1,138 @@ +% Options for packages loaded elsewhere +\PassOptionsToPackage{unicode}{hyperref} +\PassOptionsToPackage{hyphens}{url} +\documentclass[ + a4paper, +]{article} +\usepackage{xcolor} +\usepackage{amsmath,amssymb} +\setcounter{secnumdepth}{-\maxdimen} % remove section numbering +\usepackage{iftex} +\ifPDFTeX + \usepackage[T1]{fontenc} + \usepackage[utf8]{inputenc} + \usepackage{textcomp} % provide euro and other symbols +\else % if luatex or xetex + \usepackage{unicode-math} % this also loads fontspec + \defaultfontfeatures{Scale=MatchLowercase} + \defaultfontfeatures[\rmfamily]{Ligatures=TeX,Scale=1} +\fi +\usepackage{lmodern} +\ifPDFTeX\else + % xetex/luatex font selection +\fi +% Use upquote if available, for straight quotes in verbatim environments +\IfFileExists{upquote.sty}{\usepackage{upquote}}{} +\IfFileExists{microtype.sty}{% use microtype if available + \usepackage[]{microtype} + \UseMicrotypeSet[protrusion]{basicmath} % disable protrusion for tt fonts +}{} +\makeatletter +\@ifundefined{KOMAClassName}{% if non-KOMA class + \IfFileExists{parskip.sty}{% + \usepackage{parskip} + }{% else + \setlength{\parindent}{0pt} + \setlength{\parskip}{6pt plus 2pt minus 1pt}} +}{% if KOMA class + \KOMAoptions{parskip=half}} +\makeatother +\setlength{\emergencystretch}{3em} % prevent overfull lines +\providecommand{\tightlist}{% + \setlength{\itemsep}{0pt}\setlength{\parskip}{0pt}} +\usepackage{bookmark} +\IfFileExists{xurl.sty}{\usepackage{xurl}}{} % add URL line breaks if available +\urlstyle{same} +\hypersetup{ + hidelinks, + pdfcreator={LaTeX via pandoc}} + +\author{} +\date{} + +\begin{document} + +\section{Database Consistency Test}\label{database-consistency-test} + +\section{Introduction}\label{introduction} + +This EngineeringPaper.xyz sheet shows how to calculated the maximum +stress and displacement for a cantilever beam loaded by a vertical force +at its end. The geometry of the beam is shown in the figure below. The +beam has a rectangular cross section with height h and width b. The +maximum stress in a cantilever with length much larger than its height +is due to bending stress that occurs at the base of the beam. For a +downward force, the maximum stress will be a tensile force at the top of +the beam and a compressive stress at the bottom of the beam. For beam +sections that are symmetric top to bottom, the maximum tensile and +compressive stresses will have equal magnitude. \# \# \# Select Beam +Material + +\section{Select Beam Section}\label{select-beam-section} + +Source for beam section equations and images: +\url{https://en.wikipedia.org/wiki/List_of_second_moments_of_area} + +\section{Define the Other Beam +Parameters}\label{define-the-other-beam-parameters} + +\[ l=500\left[mm\right] \] + +\[ F=500\left[N\right] \] + +\section{Define the Deflection and Stress +Equations}\label{define-the-deflection-and-stress-equations} + +The maximum displacement of a cantilever beam loaded at its end is +defined by {[}1{]}: + +\[ y_{max}=-\frac{F\cdot l^{3}}{3\cdot E\cdot I_{x}} \] + +where Ix is the area moment of inertia of the cross section about the +x-axis as selected in the table above. The maximum displacement can now +be queried: + +\[\boxed{ y_{max}=\left[mm\right] =-0.165297794307557 \left[mm\right] }\] + +The maximum bending stress in a beam section is given by: + +\[ \sigma_{max}=\frac{M\cdot c}{I_{x}} \] + +where M is the bending moment at the section of interest and c is +distance from the neutral axis of the beam to the top or bottom of the +beam. The maximum moment occurs at the base of the beam and is defined +by: + +\[ M=l\cdot F \] + +where l is the length of the beam. Now that everything is defined, the +maximum stress can be queried and converted to {[}MPa{]} units: + +\[\boxed{ \sigma_{max}=\left[MPa\right] =5.33333333333333 \left[MPa\right] }\] + +These results can be compared to the results obtained from a finite +element analysis using SolidWorks. The beam has the same dimensions, +loading, and material as the rectangular cross-section and the aluminum +table options above. The displacement plot is shown below. The maximum +displacement magnitude obtained using finite element analysis is .175 +{[}mm{]}, which is close to the .165 {[}mm{]} obtained using the +equations above. + +Similarly, the maximum stress calculation can be compared to the +SolidWorks results. The maximum stress obtained from SolidWorks is 6.5 +{[}MPa{]} as shown below. Note that this is higher than the 5.3 +{[}MPa{]} obtained using the beam equation. This is the result of the +stress concentration that occurs at the based of the cantilever where it +meets the fixed boundary condition. A littler further from the fixed +boundary condition, the finite element values are close to the +calculated values. + +{[}1{]} Norton, R. L. +``\href{https://www.pearson.com/us/higher-education/program/Norton-Machine-Design-5th-Edition/PGM275676.html}{Machine +design. A integrated approach, 5th Editi.}'' (2013). + +\[ y=\frac{F}{6\cdot E\cdot I_{x}}\cdot\left(x^{3}-3\cdot l\cdot x^{2}\right) \] + +Created with \href{https://engineeringpaper.xyz}{EngineeringPaper.xyz} + +\end{document} diff --git a/tests/test_main.py b/tests/test_main.py index c0d88d8..f3c5784 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -38,6 +38,17 @@ def test_md_to_docx(client): assert filecmp.cmp('./tests/output/output.docx', './tests/output_reference.docx', shallow=False) +def test_md_to_docx_a4(client): + files = {'request_file': ("input.md", open('./tests/input.md', 'rb'), "text/markdown")} + response = client.post("/docgen/docx_a4", files=files) + assert response.status_code == 200 + + # save docx file as artifact + with open('./tests/output/output_a4.docx', 'wb') as output_docx: + output_docx.write(response.content) + + assert filecmp.cmp('./tests/output/output_a4.docx', './tests/output_reference_a4.docx', shallow=False) + def test_md_to_pdf(client): files = {'request_file': ("input.md", open('./tests/input_with_unicode.md', 'rb'), "text/markdown")} response = client.post("/docgen/pdf", files=files) @@ -50,17 +61,64 @@ def test_md_to_pdf(client): # file comparison not working (change identifier metadata?) so will just check size assert len(response.content) > 200000 + # Check paper size using pdfinfo + pdfinfo_result = subprocess.run(['pdfinfo', './tests/output/output.pdf'], capture_output=True, text=True) + assert pdfinfo_result.returncode == 0 + + letter_size = False + for line in pdfinfo_result.stdout.splitlines(): + if "letter" in line: + letter_size = True + break + + assert(letter_size) + +def test_md_to_pdf_a4(client): + files = {'request_file': ("input.md", open('./tests/input_with_unicode.md', 'rb'), "text/markdown")} + response = client.post("/docgen/pdf_a4", files=files) + assert response.status_code == 200 + + # save pdf file as artifact + with open('./tests/output/output.pdf', 'wb') as output_pdf: + output_pdf.write(response.content) + + # file comparison not working (change identifier metadata?) so will just check size + assert len(response.content) > 200000 + + # Check paper size using pdfinfo + pdfinfo_result = subprocess.run(['pdfinfo', './tests/output/output.pdf'], capture_output=True, text=True) + assert pdfinfo_result.returncode == 0 + + a4_size = False + for line in pdfinfo_result.stdout.splitlines(): + if "A4" in line: + a4_size = True + break + + assert(a4_size) + def test_md_to_latex(client): files = {'request_file': ("input.md", open('./tests/input.md', 'rb'), "text/markdown")} response = client.post("/docgen/tex", files=files) assert response.status_code == 200 - # save pdf file as artifact + # save md file as artifact with open('./tests/output/output.tex', 'wb') as output_pdf: output_pdf.write(response.content) assert filecmp.cmp('./tests/output/output.tex', './tests/output_reference.tex', shallow=False) +def test_md_to_latex_a4(client): + files = {'request_file': ("input.md", open('./tests/input.md', 'rb'), "text/markdown")} + response = client.post("/docgen/tex_a4", files=files) + assert response.status_code == 200 + + # save md file as artifact + with open('./tests/output/output_a4.tex', 'wb') as output_pdf: + output_pdf.write(response.content) + + assert filecmp.cmp('./tests/output/output_a4.tex', './tests/output_reference_a4.tex', shallow=False) + def test_error_on_binary_input(client): files = {'request_file': ("input.md", open('./tests/output_reference.docx', 'rb'), "text/markdown")} response = client.post("/docgen/docx", files=files)