44import os
55import re
66import shutil
7+ import textwrap
78
89
910PRODUCT_NAME = "TemplateProject"
1011
1112SCRIPT_DIR = os .path .dirname (os .path .realpath (__file__ ))
1213PROJECT_DIR = os .path .abspath (os .path .join (SCRIPT_DIR , os .pardir ))
1314INSTALLER_DIR = os .path .join (PROJECT_DIR , "installer" )
15+ MANUAL_DIR = os .path .join (PROJECT_DIR , "manual" )
1416
1517
1618def read_text (path ):
@@ -50,6 +52,16 @@ def markdown_to_plain_text(markdown):
5052 return "\n " .join (lines ).strip () + "\n "
5153
5254
55+ def markdown_inline_to_text (markdown ):
56+ text = re .sub (r"\[([^\]]+)\]\([^\)]+\)" , r"\1" , markdown )
57+ text = re .sub (r"\*\*([^*]+)\*\*" , r"\1" , text )
58+ text = re .sub (r"__([^_]+)__" , r"\1" , text )
59+ text = re .sub (r"\*([^*]+)\*" , r"\1" , text )
60+ text = re .sub (r"_([^_]+)_" , r"\1" , text )
61+ text = re .sub (r"`([^`]+)`" , r"\1" , text )
62+ return text .strip ()
63+
64+
5365def rtf_escape (text ):
5466 escaped = []
5567
@@ -95,8 +107,169 @@ def markdown_file_to_plain_text(name):
95107 return markdown_to_plain_text (read_text (os .path .join (INSTALLER_DIR , name + ".md" )))
96108
97109
110+ def pdf_text (text ):
111+ text = text .encode ("cp1252" , "replace" ).decode ("cp1252" )
112+ return text .replace ("\\ " , "\\ \\ " ).replace ("(" , "\\ (" ).replace (")" , "\\ )" )
113+
114+
115+ def write_pdf (path , page_streams ):
116+ os .makedirs (os .path .dirname (path ), exist_ok = True )
117+
118+ objects = [
119+ b"<< /Type /Catalog /Pages 2 0 R >>" ,
120+ None ,
121+ b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>" ,
122+ b"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica-Bold >>" ,
123+ ]
124+
125+ page_refs = []
126+ for stream in page_streams :
127+ stream_bytes = stream .encode ("cp1252" , "replace" )
128+ page_object_id = len (objects ) + 1
129+ stream_object_id = len (objects ) + 2
130+ page_refs .append (f"{ page_object_id } 0 R" )
131+ objects .append (
132+ (
133+ "<< /Type /Page /Parent 2 0 R /MediaBox [0 0 595 842] "
134+ "/Resources << /Font << /F1 3 0 R /F2 4 0 R >> >> "
135+ f"/Contents { stream_object_id } 0 R >>"
136+ ).encode ("ascii" )
137+ )
138+ objects .append (
139+ b"<< /Length " + str (len (stream_bytes )).encode ("ascii" ) + b" >>\n stream\n " +
140+ stream_bytes + b"\n endstream"
141+ )
142+
143+ objects [1 ] = (
144+ f"<< /Type /Pages /Kids [{ ' ' .join (page_refs )} ] /Count { len (page_refs )} >>"
145+ ).encode ("ascii" )
146+
147+ output = bytearray (b"%PDF-1.4\n %\xe2 \xe3 \xcf \xd3 \n " )
148+ offsets = [0 ]
149+
150+ for index , content in enumerate (objects , start = 1 ):
151+ offsets .append (len (output ))
152+ output .extend (f"{ index } 0 obj\n " .encode ("ascii" ))
153+ output .extend (content )
154+ output .extend (b"\n endobj\n " )
155+
156+ xref_offset = len (output )
157+ output .extend (f"xref\n 0 { len (objects ) + 1 } \n " .encode ("ascii" ))
158+ output .extend (b"0000000000 65535 f \n " )
159+ for offset in offsets [1 :]:
160+ output .extend (f"{ offset :010d} 00000 n \n " .encode ("ascii" ))
161+ output .extend (
162+ (
163+ "trailer\n "
164+ f"<< /Size { len (objects ) + 1 } /Root 1 0 R >>\n "
165+ "startxref\n "
166+ f"{ xref_offset } \n "
167+ "%%EOF\n "
168+ ).encode ("ascii" )
169+ )
170+
171+ with open (path , "wb" ) as output_file :
172+ output_file .write (output )
173+
174+
175+ def write_markdown_pdf (markdown_path , pdf_path ):
176+ markdown = read_text (markdown_path )
177+ page_width = 595
178+ page_height = 842
179+ margin = 54
180+ page_streams = []
181+ stream_lines = []
182+ y = page_height - margin
183+
184+ def new_page ():
185+ nonlocal stream_lines , y
186+ if stream_lines :
187+ page_streams .append ("\n " .join (stream_lines ))
188+ stream_lines = []
189+ y = page_height - margin
190+
191+ def add_vertical_space (points ):
192+ nonlocal y
193+ if y - points < margin :
194+ new_page ()
195+ else :
196+ y -= points
197+
198+ def add_text (text , font = "F1" , size = 11 , leading = 15 , indent = 0 ):
199+ nonlocal y
200+ max_chars = max (10 , int ((page_width - (2 * margin ) - indent ) / (size * 0.52 )))
201+ wrapped_lines = textwrap .wrap (text , width = max_chars ) or ["" ]
202+
203+ for line in wrapped_lines :
204+ if y - leading < margin :
205+ new_page ()
206+ x = margin + indent
207+ stream_lines .append (f"BT /{ font } { size } Tf { x } { y :.2f} Td ({ pdf_text (line )} ) Tj ET" )
208+ y -= leading
209+
210+ paragraph_lines = []
211+ in_code_block = False
212+
213+ def flush_paragraph ():
214+ nonlocal paragraph_lines
215+ if paragraph_lines :
216+ add_text (markdown_inline_to_text (" " .join (paragraph_lines )))
217+ add_vertical_space (6 )
218+ paragraph_lines = []
219+
220+ for raw_line in markdown .splitlines ():
221+ stripped = raw_line .strip ()
222+
223+ if stripped .startswith ("```" ):
224+ flush_paragraph ()
225+ in_code_block = not in_code_block
226+ continue
227+
228+ if in_code_block :
229+ add_text (stripped , size = 10 , leading = 13 , indent = 12 )
230+ continue
231+
232+ if not stripped :
233+ flush_paragraph ()
234+ continue
235+
236+ heading = re .match (r"^(#{1,6})\s+(.+)$" , stripped )
237+ bullet = re .match (r"^[-*+]\s+(.+)$" , stripped )
238+ numbered = re .match (r"^(\d+\.)\s+(.+)$" , stripped )
239+
240+ if heading :
241+ flush_paragraph ()
242+ level = len (heading .group (1 ))
243+ size = 22 if level == 1 else 16 if level == 2 else 13
244+ add_vertical_space (8 if y < page_height - margin else 0 )
245+ add_text (markdown_inline_to_text (heading .group (2 )), font = "F2" , size = size , leading = size + 6 )
246+ add_vertical_space (6 )
247+ elif bullet :
248+ flush_paragraph ()
249+ add_text ("- " + markdown_inline_to_text (bullet .group (1 )), indent = 12 )
250+ elif numbered :
251+ flush_paragraph ()
252+ add_text (numbered .group (1 ) + " " + markdown_inline_to_text (numbered .group (2 )), indent = 12 )
253+ else :
254+ paragraph_lines .append (stripped )
255+
256+ flush_paragraph ()
257+ if not stream_lines :
258+ add_text (" " )
259+ page_streams .append ("\n " .join (stream_lines ))
260+ write_pdf (pdf_path , page_streams )
261+
262+
263+ def build_manual_pdf (build_dir_name ):
264+ source_path = os .path .join (MANUAL_DIR , PRODUCT_NAME + " manual.md" )
265+ target_path = os .path .join (PROJECT_DIR , build_dir_name , "manual" , PRODUCT_NAME + " manual.pdf" )
266+ write_markdown_pdf (source_path , target_path )
267+ print ("Prepared manual PDF at " + target_path )
268+
269+
98270def build_mac_docs ():
99271 target_dir = os .path .join (PROJECT_DIR , "build-mac" , "installer" , "resources" )
272+ build_manual_pdf ("build-mac" )
100273
101274 for name in ("license" , "readme-mac" , "intro" ):
102275 source_text = markdown_file_to_plain_text (name )
@@ -112,6 +285,7 @@ def build_mac_docs():
112285
113286def build_win_docs ():
114287 target_dir = os .path .join (PROJECT_DIR , "build-win" , "installer-docs" )
288+ build_manual_pdf ("build-win" )
115289
116290 for name in ("license" , "readme-win" , "readme-win-demo" ):
117291 write_text (os .path .join (target_dir , name + ".txt" ), markdown_file_to_plain_text (name ))
0 commit comments