· last year · Dec 19, 2023, 08:50 AM
1"""PDF Parts Manual Creation Tool
2
3This script creates a Parts Manual PDF from a Job Materials CSV file
4generated by Infor CSI. This script accepts CSV files encoded in
5UTF-16 LE. Name the CSV file "ToExcel_JobMaterials.csv" and save it to
6the folder containing this script.
7
8This script contains the following functions:
9 merge_pages: Merges all of the P-Pages to be included in the manual
10 into one file.
11 collect_toc_data: Iterates through the job materials dataframe
12 gathering information to fill in the rows of the table of contents.
13 build_page_nums: Builds a PDF file with page numbers printed in the
14 center of the bottom margin.
15 zerofy_rotation: Sets the merged P-Pages rotation back to zero
16 without changing the P-Pages' appearance.
17 stamp_page_nums: Overlays the page numbers on top of the merged
18 P-Pages to add page numbers to the manual.
19 build_toc: Creates a PDF file containing only the table of contents
20 pages.
21 combine_files: Compiles the final PDF file of the parts manual.
22"""
23__version__ = '1.4'
24__author__ = 'toodahlou'
25
26from fitz import (Document,
27 PDF_ENCRYPT_KEEP)
28from pandas import (concat,
29 DataFrame,
30 read_csv)
31from pathlib import Path
32from reportlab.lib.colors import (black,
33 HexColor)
34from reportlab.lib.enums import TA_CENTER
35from reportlab.lib.pagesizes import letter
36from reportlab.lib.styles import (getSampleStyleSheet,
37 ParagraphStyle)
38from reportlab.lib.units import inch
39from reportlab.pdfgen import canvas
40from reportlab.platypus import (Paragraph,
41 SimpleDocTemplate,
42 Spacer,
43 Table,
44 TableStyle)
45
46
47def merge_pages(job_matl: DataFrame) -> tuple[Document, DataFrame]:
48 """Increments thorugh the job materials dataframe. If the P-Page
49 exists in PDFPlot, it is added to the returned Document object. If
50 the P-Page does not exist, its file path is added to the returned
51 DataFrame object.
52
53 Args:
54 job_matl (DataFrame): Dataframe created from an Infor CSI Job
55 Materials csv file by pandas.read_csv()
56
57 Returns:
58 tuple[Document, DataFrame]: Document object containing the
59 merged P-Page pdfs. Dataframe object containing paths of any
60 missing P-Pages.
61 """
62 pages_out = Document()
63 files_not_found = DataFrame()
64
65 for j in enumerate(job_matl.index):
66 page_num = str(job_matl.loc[job_matl.index[j[0]], 'Material'])
67 page_pdf = Path(f'F:/_ProductionMaster/PDFPlot/{page_num}-C1.pdf')
68 if page_pdf.exists():
69 pages_out.insert_pdf(Document(page_pdf))
70 else:
71 page_desc = job_matl.loc[job_matl.index[j[0]],
72 'Material Description']
73 missing_file = DataFrame({'File Not Found': [page_pdf],
74 'Description': [page_desc]})
75 files_not_found = concat([files_not_found, missing_file],
76 ignore_index=True)
77
78 return pages_out, files_not_found
79
80
81def collect_toc_data(job_matl: DataFrame,
82 manual_page_num: int = 1) -> DataFrame:
83 """Iterates through the job materials dataframe gathering
84 information to fill in the rows of the table of contents.
85
86 Args:
87 job_matl (DataFrame): Dataframe created from an Infor CSI Job
88 Materials csv file by pandas.read_csv()
89 manual_page_num (int, optional): The first number to use for
90 page numbers. Defaults to 1.
91
92 Returns:
93 DataFrame: The table of contents data. Columns are
94 ['Seq.': The BOM Sequence of the P-Page,
95 'Description': The P-Page description,
96 'No.': The P-Page number,
97 'Page': The page number of the manual the P-Page begins]
98 """
99 toc_data = DataFrame()
100
101 for j in enumerate(job_matl.index):
102 page_num = str(job_matl.loc[job_matl.index[j[0]], 'Material'])
103 page_pdf = Path(f'F:/_ProductionMaster/PDFPlot/{page_num}-C1.pdf')
104 page_desc = job_matl.loc[job_matl.index[j[0]], 'Material Description']
105
106 toc_row = DataFrame({'Seq.': [j[0]+1],
107 'Description': [page_desc],
108 'No.': [page_num],
109 'Page': [manual_page_num]})
110 toc_data = concat([toc_data, toc_row],
111 ignore_index=True)
112
113 manual_page_num = manual_page_num + Document(page_pdf).page_count
114
115 return toc_data
116
117
118def build_page_nums(toc_data: DataFrame) -> None:
119 """Finds the page the last P-Page begins on in the manual and
120 calculates the last page number the manual will have. Builds a PDF
121 file with page numbers printed in the center of the bottom margin.
122
123 Args:
124 toc_data (DataFrame): The table of contents data. Requires 'No.'
125 and 'Page' columns in the DataFrame.
126 """
127 c = canvas.Canvas('page_nums_temp.pdf',
128 pagesize=letter)
129 last_page = toc_data.loc[toc_data.index[-1], 'No.']
130 last_page_path = Path(f'F:/_ProductionMaster/PDFPlot/{last_page}-C1.pdf')
131 last_page_num = (toc_data['Page'].max()
132 + Document(last_page_path).page_count
133 - 1)
134
135 for j in range(last_page_num):
136 c.setFont(psfontname='Helvetica',
137 size=8)
138 c.translate(4.25 * inch,
139 0.5 * inch)
140 c.drawCentredString(0,
141 0,
142 f'{j+1}')
143 c.showPage()
144 c.save()
145
146 return None
147
148
149def zerofy_rotation(merged_pages_file: str | Path) -> None:
150 """Overlays the contents of the merged_pages_file on top of a blank
151 page to set the page's rotation back to zero without changing the
152 page's appearance.
153
154 Args:
155 merged_pages_file (str | Path): PDF file containing all of the
156 P-Pages to be included in the manual.
157 """
158 merged_pages = Document(merged_pages_file)
159 zerofy_pages = Document()
160
161 for j in range(merged_pages.page_count):
162 background_page = zerofy_pages.new_page(width=8.5 * inch,
163 height=11 * inch)
164
165 # Create a document to stamp onto the blank page
166 stamp = Document()
167 stamp.insert_pdf(merged_pages,
168 from_page=j,
169 to_page=j)
170
171 background_page.show_pdf_page(background_page.rect,
172 stamp,
173 pno=0,
174 keep_proportion=True,
175 overlay=True,
176 oc=0,
177 rotate=0,
178 clip=None)
179
180 merged_pages.close
181 zerofy_pages.save(merged_pages_file)
182
183 return None
184
185
186def stamp_page_nums(merged_pages_file: str | Path,
187 page_nums_file: str | Path) -> None:
188 """Overlays the contents of the page_nums_file on top of the
189 merged_pages_file to add page numbers to the manual.
190
191 Args:
192 merged_pages_file (str | Path): PDF file containing all of the
193 P-Pages to be included in the manual.
194 page_nums_file (str | Path): PDF file containing the page numbers
195 """
196 merged_pages = Document(merged_pages_file)
197 page_nums = Document(page_nums_file)
198
199 for j in range(merged_pages.page_count):
200 background_page = merged_pages.load_page(j)
201
202 # Create a document to stamp onto the merged_pages page
203 stamp = Document()
204 stamp.insert_pdf(page_nums,
205 from_page=j,
206 to_page=j)
207
208 background_page.show_pdf_page(background_page.rect,
209 stamp,
210 pno=0,
211 keep_proportion=True,
212 overlay=True,
213 oc=0,
214 rotate=-background_page.rotation,
215 clip=None)
216
217 merged_pages.save(merged_pages_file,
218 incremental=True,
219 encryption=PDF_ENCRYPT_KEEP)
220
221 return None
222
223
224def build_toc(toc_data: DataFrame,
225 toc_main_header: str,
226 toc_sub_header: str) -> None:
227 """Creates a PDF file containing only the table of contents pages.
228
229 Args:
230 toc_data (DataFrame): The table of contents data
231 toc_main_header (str): The larger heading to be printed on the
232 first line of the table of contents header
233 toc_sub_header (str): The smaller heading to be printed on the
234 second line of the table of contents header
235 """
236 text_styles = getSampleStyleSheet()
237 text_styles.add(ParagraphStyle(name='main_header',
238 alignment=TA_CENTER,
239 fontName='Helvetica-Bold',
240 fontSize=18))
241 text_styles.add(ParagraphStyle(name='sub_header',
242 alignment=TA_CENTER,
243 fontName='Helvetica-Bold',
244 fontSize=14))
245 table_style = TableStyle([('FONTNAME', (0, 0), (3, 0), 'Helvetica-Bold'),
246 ('FONTSIZE', (0, 0), (3, 0), 12),
247 ('ALIGNMENT', (0, 0), (3, 0), 'CENTER'),
248 ('VALIGN', (0, 0), (3, 0), 'TOP'),
249 ('BACKGROUND', (0, 0), (3, 0),
250 HexColor('#FF671F')),
251 ('BOX', (0, 0), (3, 0), 1, black),
252 ('FONTNAME', (0, 1), (-1, -1), 'Helvetica'),
253 ('FONTSIZE', (0, 1), (-1, -1), 11),
254 ('ALIGNMENT', (0, 1), (0, -1), 'RIGHT'),
255 ('ALIGNMENT', (1, 1), (1, -1), 'LEFT'),
256 ('ALIGNMENT', (2, 1), (2, -1), 'CENTER'),
257 ('ALIGNMENT', (3, 1), (3, -1), 'RIGHT')
258 ])
259
260 toc = SimpleDocTemplate('TOC_temp.pdf',
261 pagesize=letter,
262 rightMargin=0.5 * inch,
263 leftMargin=0.5 * inch,
264 topMargin=0.5 * inch,
265 bottomMargin=0.5 * inch)
266
267 toc_list = toc_data.values.tolist()
268 toc_list.insert(0,
269 toc_data.columns.tolist())
270
271 toc_document = list()
272 toc_document.append(Paragraph(toc_main_header,
273 text_styles['main_header']))
274 toc_document.append(Spacer(1, 12))
275 toc_document.append(Paragraph(toc_sub_header,
276 text_styles['sub_header']))
277 toc_document.append(Spacer(1, 12))
278 toc_document.append(Table(toc_list,
279 colWidths=(0.5 * inch, # Seq.
280 4.5 * inch, # Description
281 1 * inch, # No.
282 0.5 * inch), # Page
283 style=table_style,
284 repeatRows=1))
285 toc.build(toc_document)
286
287 return None
288
289
290def combine_files(machine_type: str,
291 toc_sub_header: str,
292 toc_file: str | Path,
293 merged_pages_file: str | Path) -> None:
294 """Compiles the final PDF file of the parts manual.
295
296 Args:
297 machine_type (str): Selects the front cover PDF file to be
298 added to the manual
299 toc_sub_header (str): The file name the manual will be saved as
300 toc_file (str | Path): PDF file containing the table of
301 contents pages
302 merged_pages_file (str | Path): PDF file containing all of the
303 P-Pages to be included in the manual. P-Pages should already be
304 stamped with page numbers.
305 """
306 output = Document()
307
308 cover_path = Path(f'F:/_ProductionMaster/PDFPlot/{machine_type}-CVR.pdf')
309 output.insert_pdf(Document(cover_path))
310
311 toc_doc = Document(toc_file)
312
313 # Add a blank page before the table of contents if the TOC contains
314 # an odd number of pages. This will ensure the P-Page pictorial
315 # stays on the left side of the facing pages.
316 if not toc_doc.page_count % 2 == 0:
317 output.new_page(width=8.5 * inch,
318 height=11 * inch)
319
320 output.insert_pdf(toc_doc)
321
322 page_num_start = output.page_count
323
324 output.insert_pdf(Document(merged_pages_file))
325
326 footer_path = Path(f'F:/_ProductionMaster/PDFPlot/Datasheet Footer.pdf')
327 output.insert_pdf(Document(footer_path))
328
329 output.set_page_labels([{'startpage': 0,
330 'prefix': '',
331 'style': 'r',
332 'firstpagenum': 1},
333 {'startpage': page_num_start,
334 'prefix': '',
335 'style': 'D',
336 'firstpagenum': 1}])
337
338 output.save(f'{toc_sub_header}.pdf',
339 garbage=4,
340 deflate=True,
341 deflate_images=True,
342 deflate_fonts=True)
343
344 return None
345
346
347# BEGIN MAIN PROGRAM
348
349MachineType = str()
350TOCMainHeader = str()
351TOCSubHeader = str()
352Answer = str()
353
354# Ask users to input machine information.
355# Avoid case sensitivity for Answer.
356while not Answer.lower().startswith('y'):
357 MachineType = input('Enter the machine type (ex. M1E4, FP5): ')
358 TOCMainHeader = input('Enter the Table of Contents main header: ')
359 TOCSubHeader = input('Enter the Table of Contents sub-header: ')
360
361 print(f'You have entered:'
362 f'\nMachine type: {MachineType}'
363 f'\nMain Header: {TOCMainHeader}'
364 f'\nSub Header: {TOCSubHeader}')
365
366 # Give the users a chance to re-enter the info if there's a typo.
367 Answer = input('Is this correct?: ')
368
369JobMaterials = read_csv(Path('ToExcel_JobMaterials.csv'),
370 sep="\t",
371 encoding='UTF-16 LE')
372
373# Filter JobMaterials for the P-Pages and S-Pages that we want to keep.
374LowercaseSeries = JobMaterials['Material'].str.lower()
375
376# Remove lines that aren't P-Pages.
377JobMaterialsP = JobMaterials.loc[LowercaseSeries.str.startswith('p')]
378
379# Remove lines that aren't S-Pages.
380JobMaterialsS = JobMaterials.loc[LowercaseSeries.str.startswith('s')]
381
382# Now, put the P-page and S-page lists together.
383JobMaterials = concat([JobMaterialsP, JobMaterialsS],
384 ignore_index=True)
385
386JobMaterials.sort_values(by=['BOM Seq'],
387 inplace=True)
388
389MergedPages, FilesNotFound = merge_pages(JobMaterials)
390
391if not len(FilesNotFound.index) == 0:
392 FilesNotFound.to_csv(Path('files_not_found.csv'))
393 print(f'Some files were not found.'
394 f'Check files_not_found for the list of missing files.')
395
396else:
397 MergedPagesFilename = f'merged_pages_temp.pdf'
398 PageNumbersFilename = f'page_nums_temp.pdf'
399 TOCFilename = f'TOC_temp.pdf'
400
401 MergedPages.save(MergedPagesFilename)
402 MergedPages.close()
403
404 TOCData = collect_toc_data(JobMaterials)
405 build_page_nums(TOCData)
406 zerofy_rotation(MergedPagesFilename)
407 stamp_page_nums(MergedPagesFilename,
408 PageNumbersFilename)
409 build_toc(TOCData,
410 TOCMainHeader,
411 TOCSubHeader)
412 combine_files(MachineType,
413 TOCSubHeader,
414 TOCFilename,
415 MergedPagesFilename)
416
417 print(f'Parts manual created.')
418
419input(f'Press enter to end this script.')
420