Yehor commited on
Commit
39d4462
·
1 Parent(s): d30a68e
Files changed (10) hide show
  1. .dockerignore +4 -0
  2. .gitattributes +0 -35
  3. .gitignore +6 -0
  4. Dockerfile +67 -0
  5. README.md +65 -1
  6. app.py +189 -0
  7. demo.jpeg +0 -0
  8. document-template.typ +32 -0
  9. requirements-dev.txt +1 -0
  10. requirements.txt +2 -0
.dockerignore ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ .ruff_cache/
2
+ .venv/
3
+
4
+ typst
.gitattributes DELETED
@@ -1,35 +0,0 @@
1
- *.7z filter=lfs diff=lfs merge=lfs -text
2
- *.arrow filter=lfs diff=lfs merge=lfs -text
3
- *.bin filter=lfs diff=lfs merge=lfs -text
4
- *.bz2 filter=lfs diff=lfs merge=lfs -text
5
- *.ckpt filter=lfs diff=lfs merge=lfs -text
6
- *.ftz filter=lfs diff=lfs merge=lfs -text
7
- *.gz filter=lfs diff=lfs merge=lfs -text
8
- *.h5 filter=lfs diff=lfs merge=lfs -text
9
- *.joblib filter=lfs diff=lfs merge=lfs -text
10
- *.lfs.* filter=lfs diff=lfs merge=lfs -text
11
- *.mlmodel filter=lfs diff=lfs merge=lfs -text
12
- *.model filter=lfs diff=lfs merge=lfs -text
13
- *.msgpack filter=lfs diff=lfs merge=lfs -text
14
- *.npy filter=lfs diff=lfs merge=lfs -text
15
- *.npz filter=lfs diff=lfs merge=lfs -text
16
- *.onnx filter=lfs diff=lfs merge=lfs -text
17
- *.ot filter=lfs diff=lfs merge=lfs -text
18
- *.parquet filter=lfs diff=lfs merge=lfs -text
19
- *.pb filter=lfs diff=lfs merge=lfs -text
20
- *.pickle filter=lfs diff=lfs merge=lfs -text
21
- *.pkl filter=lfs diff=lfs merge=lfs -text
22
- *.pt filter=lfs diff=lfs merge=lfs -text
23
- *.pth filter=lfs diff=lfs merge=lfs -text
24
- *.rar filter=lfs diff=lfs merge=lfs -text
25
- *.safetensors filter=lfs diff=lfs merge=lfs -text
26
- saved_model/**/* filter=lfs diff=lfs merge=lfs -text
27
- *.tar.* filter=lfs diff=lfs merge=lfs -text
28
- *.tar filter=lfs diff=lfs merge=lfs -text
29
- *.tflite filter=lfs diff=lfs merge=lfs -text
30
- *.tgz filter=lfs diff=lfs merge=lfs -text
31
- *.wasm filter=lfs diff=lfs merge=lfs -text
32
- *.xz filter=lfs diff=lfs merge=lfs -text
33
- *.zip filter=lfs diff=lfs merge=lfs -text
34
- *.zst filter=lfs diff=lfs merge=lfs -text
35
- *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
.gitignore ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ .ruff_cache/
2
+ .venv/
3
+
4
+ __pycache__/
5
+
6
+ document.pdf
Dockerfile ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM ubuntu:24.04
2
+
3
+ ENV DEBIAN_FRONTEND=noninteractive
4
+
5
+ RUN apt-get update && \
6
+ apt-get upgrade -y && \
7
+ apt-get install -y --no-install-recommends \
8
+ git \
9
+ git-lfs \
10
+ wget \
11
+ curl \
12
+ # python build dependencies \
13
+ build-essential \
14
+ libssl-dev \
15
+ zlib1g-dev \
16
+ libbz2-dev \
17
+ libreadline-dev \
18
+ libsqlite3-dev \
19
+ libncursesw5-dev \
20
+ xz-utils \
21
+ tk-dev \
22
+ libxml2-dev \
23
+ libxmlsec1-dev \
24
+ libffi-dev \
25
+ liblzma-dev
26
+
27
+ RUN apt install -y --reinstall ca-certificates && update-ca-certificates -f
28
+
29
+ RUN useradd -m -u 1001 user
30
+ USER user
31
+ ENV HOME=/home/user
32
+ ENV PATH=/home/user/.local/bin:${PATH}
33
+ WORKDIR ${HOME}/app
34
+
35
+ RUN wget "https://github.com/typst/typst/releases/download/v0.12.0/typst-x86_64-unknown-linux-musl.tar.xz" && \
36
+ tar -xf typst-x86_64-unknown-linux-musl.tar.xz && \
37
+ mv typst-x86_64-unknown-linux-musl/typst . && \
38
+ rm -rf typst-x86_64-unknown-linux-musl && \
39
+ rm typst-x86_64-unknown-linux-musl.tar.xz
40
+
41
+ RUN curl https://pyenv.run | bash
42
+
43
+ ENV PATH=${HOME}/.pyenv/shims:${HOME}/.pyenv/bin:${PATH}
44
+
45
+ ARG PYTHON_VERSION=3.13.1
46
+ RUN pyenv install ${PYTHON_VERSION} && \
47
+ pyenv global ${PYTHON_VERSION} && \
48
+ pyenv rehash && \
49
+ pip install --no-cache-dir -U pip setuptools wheel && \
50
+ pip install packaging ninja
51
+
52
+ COPY --chown=1000 ./requirements.txt /tmp/requirements.txt
53
+ RUN pip install --no-cache-dir --upgrade -r /tmp/requirements.txt
54
+
55
+ COPY --chown=1000 . ${HOME}/app
56
+ ENV PYTHONPATH=${HOME}/app \
57
+ PYTHONUNBUFFERED=1 \
58
+ GRADIO_ALLOW_FLAGGING=never \
59
+ GRADIO_NUM_PORTS=1 \
60
+ GRADIO_SERVER_PORT=7860 \
61
+ GRADIO_SERVER_NAME=0.0.0.0 \
62
+ GRADIO_THEME=huggingface \
63
+ SYSTEM=spaces
64
+
65
+ EXPOSE 7860
66
+
67
+ CMD ["python", "app.py"]
README.md CHANGED
@@ -7,4 +7,68 @@ sdk: docker
7
  pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  pinned: false
8
  ---
9
 
10
+ # Generate PDF documents using Gradio and Typst
11
+
12
+ - minijinja to template a Typst document
13
+ - Typst to generate a PDF document
14
+ - Gradio to provide a web interface with user parameters (via Components)
15
+ - Docker to run the app
16
+
17
+ ## Demo
18
+
19
+ <img src="./demo.jpeg" width="800">
20
+
21
+ ## Install
22
+
23
+ Clone the repository:
24
+
25
+ ```shell
26
+ git clone https://github.com/egorsmkv/pdf-generator-gradio
27
+ cd pdf-generator-gradio
28
+ ```
29
+
30
+ Install `typst`:
31
+
32
+ ```shell
33
+ cargo install typst-cli
34
+ ```
35
+
36
+ ## Development
37
+
38
+ Create virtual environment and install dependencies:
39
+
40
+ ```shell
41
+ uv venv --python 3.13
42
+
43
+ source .venv/bin/activate
44
+
45
+ uv pip install -r requirements.txt
46
+ uv pip install -r requirements-dev.txt
47
+ ```
48
+
49
+ ## Run
50
+
51
+ Run Gradio app locally:
52
+
53
+ ```shell
54
+ export TYPST_BIN=/home/yehor/.cargo/bin/typst
55
+
56
+ gradio app.py
57
+ ```
58
+
59
+ ## Production
60
+
61
+ Build the Docker image:
62
+
63
+ ```shell
64
+ docker build -t pdf-generator-gradio .
65
+ ```
66
+
67
+ Run:
68
+
69
+ ```shell
70
+ docker run --rm -p 7860:7860 -it pdf-generator-gradio
71
+
72
+ # Enable Gradio sharing
73
+ docker run --rm -p 7860:7860 -e DO_SHARE=y -it pdf-generator-gradio
74
+ ```
app.py ADDED
@@ -0,0 +1,189 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import sys
2
+ import subprocess
3
+ from pathlib import Path
4
+ from os import getenv
5
+
6
+ from importlib.metadata import version
7
+ from PIL import Image
8
+ from minijinja import Environment
9
+
10
+ import gradio as gr
11
+
12
+ # Config
13
+ document_name = "document-template.typ"
14
+
15
+ do_share = getenv("DO_SHARE")
16
+
17
+ concurrency_limit = getenv("CONCURRENCY_LIMIT", 1)
18
+
19
+ typst_bin_path = getenv("TYPST_BIN", "/home/user/app/typst")
20
+
21
+ # App description
22
+ title = "Typst-based PDF generation"
23
+
24
+ examples = [
25
+ """
26
+ I begin this story with a neutral statement.
27
+ Basically this is a very silly test.
28
+ """,
29
+ ]
30
+
31
+ description_head = f"""
32
+ # {title}
33
+
34
+ ## Overview
35
+
36
+ We use https://typst.app to generate a PDF file with some parameters from this Gradio app.
37
+ """.strip()
38
+
39
+ tech_env = f"""
40
+ #### Environment
41
+
42
+ - Python: {sys.version}
43
+ """.strip()
44
+
45
+
46
+ def app_version(bin_path):
47
+ return subprocess.run([bin_path, "--version"], capture_output=True, text=True)
48
+
49
+
50
+ def typst_compile(typst_bin_path, filename, export_template):
51
+ args = [typst_bin_path, "compile", filename, export_template]
52
+ print("Running:", args)
53
+
54
+ return subprocess.run(
55
+ args,
56
+ capture_output=False,
57
+ )
58
+
59
+
60
+ def convert_document(bin_paths, text):
61
+ print("Converting...")
62
+
63
+ env = Environment(
64
+ templates={
65
+ "document": Path("document-template.typ").read_text(),
66
+ }
67
+ )
68
+ formatted_document = env.render_template("document", text=text)
69
+
70
+ # Write the rendered document to a temporary file
71
+ document_file = Path("document.typ")
72
+ document_file.write_text(formatted_document)
73
+
74
+ # Compile the .typ file to a .pdf file
75
+ c = typst_compile(bin_paths["typst"], "document.typ", "document.pdf")
76
+ if c.returncode != 0:
77
+ raise gr.Error("Typst compilation failed.")
78
+
79
+ print("Result:", c)
80
+
81
+ # Extract the first page of the PDF file
82
+ c = typst_compile(bin_paths["typst"], "document.typ", "file-{n}.png")
83
+ if c.returncode != 0:
84
+ raise gr.Error("Typst exporting to PNGs failed.")
85
+
86
+ print("Result:", c)
87
+
88
+ first_page = Path("file-1.png")
89
+ if not first_page.exists():
90
+ raise gr.Error("The first page has not been exported.")
91
+
92
+ # Move the image to an object
93
+ image = Image.open(first_page.absolute())
94
+
95
+ # Remove the temporary files
96
+ first_page.unlink(missing_ok=True)
97
+ document_file.unlink(missing_ok=True)
98
+
99
+ return image
100
+
101
+
102
+ typst_version_info = app_version(typst_bin_path)
103
+ if typst_version_info.returncode != 0:
104
+ print("Error: Typst version command failed.")
105
+ exit(1)
106
+
107
+ r_tech_env = f"""
108
+ #### Typst Environment
109
+
110
+ ```
111
+ {typst_version_info.stdout.strip()}
112
+ ```
113
+ """.strip()
114
+
115
+ tech_libraries = f"""
116
+ #### Libraries
117
+
118
+ - gradio: {version("gradio")}
119
+ """.strip()
120
+
121
+
122
+ def generate_pdf(text, progress=gr.Progress()):
123
+ if not text:
124
+ raise gr.Error("Please paste your text.")
125
+
126
+ # Remove the previous PDF file and Typst file
127
+ Path("document.pdf").unlink(missing_ok=True)
128
+ Path("document.typ").unlink(missing_ok=True)
129
+
130
+ gr.Info("Generating the PDF document", duration=1)
131
+
132
+ bin_paths = {
133
+ "typst": typst_bin_path,
134
+ }
135
+ image = convert_document(bin_paths, text)
136
+
137
+ gr.Success("Finished!", duration=2)
138
+
139
+ pdf_file = gr.DownloadButton(
140
+ label="Download document.pdf",
141
+ value="./document.pdf",
142
+ visible=True,
143
+ )
144
+
145
+ return [image, pdf_file]
146
+
147
+
148
+ demo = gr.Blocks(
149
+ title=title,
150
+ analytics_enabled=False,
151
+ theme=gr.themes.Base(),
152
+ )
153
+
154
+ with demo:
155
+ gr.Markdown(description_head)
156
+
157
+ gr.Markdown("## Usage")
158
+
159
+ with gr.Row():
160
+ text = gr.Textbox(label="Text", autofocus=True, max_lines=50)
161
+
162
+ with gr.Column():
163
+ pdf_file = gr.DownloadButton(label="Download PDF", visible=False)
164
+ preview_image = gr.Image(
165
+ label="Preview image",
166
+ )
167
+
168
+ gr.Button("Generate").click(
169
+ generate_pdf,
170
+ concurrency_limit=concurrency_limit,
171
+ inputs=text,
172
+ outputs=[preview_image, pdf_file],
173
+ )
174
+
175
+ with gr.Row():
176
+ gr.Examples(label="Choose an example", inputs=text, examples=examples)
177
+
178
+ gr.Markdown("### Gradio app uses:")
179
+ gr.Markdown(tech_env)
180
+ gr.Markdown(r_tech_env)
181
+ gr.Markdown(tech_libraries)
182
+
183
+ if __name__ == "__main__":
184
+ demo.queue()
185
+
186
+ if do_share:
187
+ demo.launch(share=True)
188
+ else:
189
+ demo.launch()
demo.jpeg ADDED
document-template.typ ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ = Test
2
+
3
+ Privided text:
4
+
5
+ {{ text }}
6
+
7
+ == Another
8
+
9
+ - Writing lists in a simple way is great.
10
+ - Nothing complex, start your points with `-`
11
+ and this will become a list.
12
+ - Indented lists are created via indentation.
13
+
14
+ + Numbered lists start with `+` instead of `-`.
15
+ + There is no alternative markup syntax for lists
16
+ + So just remember `-` and `+`, all other symbols
17
+ wouldn't work in an unintended way.
18
+ + That is a general property of Typst's markup.
19
+ + Unlike Markdown, there is only one way
20
+ to write something with it.
21
+
22
+
23
+ I will just mention math ($a + b/c = sum_i x^i$)
24
+ is possible and quite pretty there:
25
+
26
+ $
27
+ 7.32 beta +
28
+ sum_(i=0)^nabla
29
+ (Q_i (a_i - epsilon)) / 2
30
+ $
31
+
32
+ To learn more about math, see corresponding chapter.
requirements-dev.txt ADDED
@@ -0,0 +1 @@
 
 
1
+ ruff
requirements.txt ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ gradio
2
+ minijinja