# SPDX-License-Identifier: LGPL-2.1-or-later # *************************************************************************** # * Copyright (c) 2024 Ondsel * # * Copyright (c) 2025 Billy Huddleston * # * * # * This program is free software; you can redistribute it and/or modify * # * it under the terms of the GNU Lesser General Public License (LGPL) * # * as published by the Free Software Foundation; either version 2 of * # * the License, or (at your option) any later version. * # * for detail see the LICENCE text file. * # * * # * This program is distributed in the hope that it will be useful, * # * but WITHOUT ANY WARRANTY; without even the implied warranty of * # * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * # * GNU Library General Public License for more details. * # * * # * You should have received a copy of the GNU Library General Public * # * License along with this program; if not, write to the Free Software * # * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 * # * USA * # * * # *************************************************************************** from string import Template import FreeCAD import Path.Log import base64 import os from Path.Main.Sanity.HTMLTemplate import ( html_template, base_template, squawk_template, tool_template, op_run_template, op_tool_template, tool_item_template, ) translate = FreeCAD.Qt.translate if False: Path.Log.setLevel(Path.Log.Level.DEBUG, Path.Log.thisModule()) Path.Log.trackModule(Path.Log.thisModule()) else: Path.Log.setLevel(Path.Log.Level.INFO, Path.Log.thisModule()) class ReportGenerator: def bytes_to_base64_with_tag(self, image_bytes, mime_type="image/png", alt="Image"): """ Takes image bytes and returns (base64_string, tag) for embedding in HTML. Default mime_type is image/png. """ if not image_bytes: return "", "" encoded_string = base64.b64encode(image_bytes).decode() html_tag = f'{alt}' return encoded_string, html_tag def __init__(self, data, embed_images=False): self.embed_images = embed_images self.squawks = "" self.tools = "" self.run_summary_ops = "" self.formatted_data = {} self.translated_labels = { "dateLabel": translate("CAM_Sanity", "Date"), "datumLabel": translate("CAM_Sanity", "Part Datum"), "descriptionLabel": translate("CAM_Sanity", "Description"), "diameterLabel": translate("CAM_Sanity", "Tool Diameter"), "feedLabel": translate("CAM_Sanity", "Feed Rate"), "fileSizeLabel": translate("CAM_Sanity", "File Size (kB)"), "fixturesLabel": translate("CAM_Sanity", "Fixtures"), "flagsLabel": translate("CAM_Sanity", "Post Processor Flags"), "gcodeFileLabel": translate("CAM_Sanity", "G-code File"), "headingLabel": translate("CAM_Sanity", "Setup Report for CAM Job"), "inspectionNotesLabel": translate("CAM_Sanity", "Inspection Notes"), "lastpostLabel": translate("CAM_Sanity", "Last Post Process Date"), "lineCountLabel": translate("CAM_Sanity", "Line Count"), "machineLabel": translate("CAM_Sanity", "Machine"), "manufLabel": translate("CAM_Sanity", "Manufacturer"), "materialLabel": translate("CAM_Sanity", "Material"), "noteLabel": translate("CAM_Sanity", "Note"), "offsetsLabel": translate("CAM_Sanity", "Work Offsets"), "opLabel": translate("CAM_Sanity", "Operation"), "operatorLabel": translate("CAM_Sanity", "Operator"), "orderByLabel": translate("CAM_Sanity", "Order By"), "outputLabel": translate("CAM_Sanity", "Output (G-code)"), "partInformationLabel": translate("CAM_Sanity", "Part Information"), "partNumberLabel": translate("CAM_Sanity", "Part Number"), "postLabel": translate("CAM_Sanity", "Postprocessor"), "programmerLabel": translate("CAM_Sanity", "Programmer"), "roughStockLabel": translate("CAM_Sanity", "Rough Stock"), "runSummaryLabel": translate("CAM_Sanity", "Run Summary"), "shapeLabel": translate("CAM_Sanity", "Tool Shape"), "speedLabel": translate("CAM_Sanity", "Spindle Speed"), "squawksLabel": translate("CAM_Sanity", "Squawks"), "stopsLabel": translate("CAM_Sanity", "Stops"), "sSpeedCarbideLabel": translate("CAM_Sanity", "Surface Speed Carbide"), "sSpeedHSSLabel": translate("CAM_Sanity", "Surface Speed HSS"), "tableOfContentsLabel": translate("CAM_Sanity", "Table of Contents"), "tcLabel": translate("CAM_Sanity", "Tool Controller"), "toolDataLabel": translate("CAM_Sanity", "Tool Data"), "toolNumberLabel": translate("CAM_Sanity", "Tool Number"), "urlLabel": translate("CAM_Sanity", "URL"), "xDimLabel": translate("CAM_Sanity", "X Size"), "yDimLabel": translate("CAM_Sanity", "Y Size"), "zDimLabel": translate("CAM_Sanity", "Z Size"), "jobMinZLabel": translate("CAM_Sanity", "Minimum Z"), "jobMaxZLabel": translate("CAM_Sanity", "Maximum Z"), "coolantLabel": translate("CAM_Sanity", "Coolant Mode"), "cycleTimeLabel": translate("CAM_Sanity", "Cycle Time"), "PartLabel": translate("CAM_Sanity", "Part"), "SequenceLabel": translate("CAM_Sanity", "Sequence"), "JobTypeLabel": translate("CAM_Sanity", "Job Type"), "CADLabel": translate("CAM_Sanity", "CAD File"), "LastSaveLabel": translate("CAM_Sanity", "Last Save"), "CustomerLabel": translate("CAM_Sanity", "Customer"), } # format the data for all blocks except tool for block in [ "baseData", "designData", "runData", "outputData", "fixtureData", "stockData", ]: for key, val in data[block].items(): Path.Log.debug(f"key: {key} val: {val}") if key == "squawkData": self._format_squawks(val) elif key == "bases": self._format_bases(val) elif key == "operations": self._format_run_summary_ops(val) elif key in ["baseimage", "imagepath", "datumImage", "stockImage"]: Path.Log.debug(f"key: {key} val: {val}") if self.embed_images: Path.Log.debug("Embedding images") if isinstance(val, bytes): encoded_image, tag = self.bytes_to_base64_with_tag( val, mime_type="image/png", alt=key ) else: encoded_image, tag = self.file_to_base64_with_tag(val) else: Path.Log.debug("Not Embedding images") tag = f"{key}" self.formatted_data[key] = tag else: self.formatted_data[key] = val # format the data for the tool block for key, val in data["toolData"].items(): if key == "squawkData": self._format_squawks(val) else: toolNumber = key toolAttributes = val # Prefer imagebytes for embedding if present if ( self.embed_images and "imagebytes" in toolAttributes and toolAttributes["imagebytes"] ): _, tag = self.bytes_to_base64_with_tag( toolAttributes["imagebytes"], mime_type="image/png", alt=key ) toolAttributes["imagepath"] = tag elif "imagepath" in toolAttributes and toolAttributes["imagepath"] != "": if self.embed_images: encoded_image, tag = self.file_to_base64_with_tag( toolAttributes["imagepath"] ) else: tag = f"{key}" toolAttributes["imagepath"] = tag self._format_tool(key, val) self.formatted_data["squawks"] = self.squawks self.formatted_data["run_summary_ops"] = self.run_summary_ops self.formatted_data["tool_data"] = self.tools self.formatted_data["tool_list"] = self._format_tool_list(data["toolData"]) # Path.Log.debug(self.formatted_data) def _format_tool_list(self, tool_data): tool_list = "" for key, val in tool_data.items(): if key == "squawkData": continue else: val["toolNumber"] = key tool_list += tool_item_template.substitute(val) return tool_list def _format_run_summary_ops(self, op_data): for op in op_data: self.run_summary_ops += op_run_template.substitute(op) def _format_tool(self, tool_number, tool_data): td = {} td["toolNumber"] = tool_number for key, val in tool_data.items(): if key == "squawkData": self._format_squawks(val) if key == "ops": opslist = "" for op in val: op.update(self.translated_labels) opslist += op_tool_template.substitute(op) td[key] = opslist else: td[key] = val td.update(self.translated_labels) Path.Log.debug(f"Tool data: {td}") self.tools += tool_template.substitute(td) def _format_bases(self, base_data): bases = "" for key, val in base_data.items(): bases += base_template.substitute({"key": key, "val": val}) self.formatted_data["bases"] = bases def _format_squawks(self, squawk_data): for squawk in squawk_data: if self.embed_images: data, tag = self.file_to_base64_with_tag(squawk["squawkIcon"]) else: tag = f"TIP" squawk["squawkIcon"] = tag self.squawks += squawk_template.substitute(squawk) def generate_html(self): self.formatted_data.update(self.translated_labels) html_content = html_template.substitute(self.formatted_data) return html_content def encode_gcode_to_base64(filepath): with open(filepath, "rb") as file: encoded_string = base64.b64encode(file.read()).decode("utf-8") return encoded_string def file_to_base64_with_tag(self, file_path): # Determine MIME type based on the file extension mime_types = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".svg": "image/svg+xml", ".gcode": "application/octet-stream", # MIME type for G-code files } extension = os.path.splitext(file_path)[1] mime_type = mime_types.get( extension, "application/octet-stream" ) # Default to binary data type if unknown if not os.path.exists(file_path): Path.Log.error(f"File not found: {file_path}") return "", "" try: # Encode file to base64 with open(file_path, "rb") as file: encoded_string = base64.b64encode(file.read()).decode() # Generate HTML tag based on file type if extension in [".jpg", ".jpeg", ".png", ".svg"]: html_tag = f'' elif extension in [".gcode", ".nc", ".tap", ".cnc"]: html_tag = f'Download G-code File' else: html_tag = f'Download File' return encoded_string, html_tag except FileNotFoundError: Path.Log.error(f"File not found: {file_path}") return "", ""