#!/usr/bin/env python
# -*- coding: utf-8 -*-
##################################################################
#
# Copyright (c) 2023 CICV, Inc. All Rights Reserved
#
##################################################################
"""
@Authors: yangzihao(yangzihao@china-icv.cn)
@Data: 2024/04/17
@Last Modified: 2024/08/01
@Summary: This file is for PDF report generate.
Refer to the CSDN: https://blog.csdn.net/cainiao_python/article/details/128928298
"""
from reportlab.pdfbase import pdfmetrics # 注册字体
from reportlab.pdfbase.ttfonts import TTFont # 字体类
from reportlab.platypus import Table, SimpleDocTemplate, Paragraph, Image, Spacer, PageBreak # 报告内容相关类
from reportlab.lib.pagesizes import A4 # 页面的标志尺寸(8.5*inch, 11*inch)
from reportlab.lib.styles import getSampleStyleSheet # 文本样式
from reportlab.lib import colors # 颜色模块
from reportlab.graphics.charts.barcharts import VerticalBarChart # 图表类
from reportlab.graphics.charts.legends import Legend # 图例类
from reportlab.graphics.shapes import Drawing # 绘图工具
from reportlab.lib.units import cm # 单位:cm
from reportlab.lib import utils
import json
import pandas as pd
# 注册字体(提前准备好字体文件, 如果同一个文件需要多种字体可以注册多个)
pdfmetrics.registerFont(TTFont('SimSun', 'SimSun.ttf'))
pdfmetrics.registerFont(TTFont('Arial', 'arial.ttf'))
class ReportPDF:
"""
This class is for PDF report generate.
"""
# 绘制标题
@staticmethod
def draw_title(title: str):
# 获取所有样式表
style = getSampleStyleSheet()
# 拿到标题样式
ct = style['Heading4']
# 单独设置样式相关属性
ct.fontName = 'SimSun' # 字体名
ct.fontSize = 18 # 字体大小
ct.leading = 24 # 行间距
ct.textColor = colors.black # 字体颜色
ct.alignment = 1 # 居中
ct.bold = True
# 创建标题对应的段落,并且返回
return Paragraph(title, ct)
# 绘制小标题
@staticmethod
def draw_little_title(title: str):
# 获取所有样式表
style = getSampleStyleSheet()
# 拿到标题样式
ct = style['Normal']
# 单独设置样式相关属性
ct.fontName = 'SimSun' # 字体名
ct.fontSize = 15 # 字体大小
# ct.leading = 15 # 行间距
ct.textColor = colors.darkblue # 字体颜色
# 创建标题对应的段落,并且返回
return Paragraph(title, ct)
# 绘制普通段落内容
@staticmethod
def draw_text(text: str, style_='Normal', font_size=10):
# 获取所有样式表
style = getSampleStyleSheet()
# 获取普通样式
ct = style[style_]
ct.fontName = 'SimSun'
ct.fontSize = font_size
ct.wordWrap = 'CJK' # 设置自动换行
ct.alignment = 0 # 左对齐
# ct.firstLineIndent = 32 # 第一行开头空格
ct.leading = 15
return Paragraph(text, ct)
# 绘制普通段落内容
@staticmethod
def draw_line_feed(text: str, font_size=10):
# 获取所有样式表
style = getSampleStyleSheet()
# 获取普通样式
ct = style['Normal']
ct.fontName = 'SimSun'
ct.fontSize = font_size
ct.textColor = colors.white
# ct.wordWrap = 'CJK' # 设置自动换行
ct.alignment = 0 # 左对齐
# ct.firstLineIndent = 32 # 第一行开头空格
ct.leading = 15
return Paragraph(text, ct)
# 绘制表格
@staticmethod
def draw_table(*args):
# 列宽度
# col_width = 120
style = [
('FONTNAME', (0, 0), (-1, -1), 'SimSun'), # 字体
('FONTSIZE', (0, 0), (-1, 0), 9), # 第一行的字体大小
('FONTSIZE', (0, 1), (-1, -1), 9), # 第二行到最后一行的字体大小
('BACKGROUND', (0, 0), (-1, 0), '#d5dae6'), # 设置第一行背景颜色
('BACKGROUND', (0, 0), (-1, 0), '#d5dae6'), # 设置第一行背景颜色
('ALIGN', (0, 0), (-1, -1), 'CENTER'), # 第一行水平居中
('ALIGN', (0, 1), (-1, -1), 'CENTER'), # 第二行到最后一行左右左对齐
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), # 所有表格上下居中对齐
('TEXTCOLOR', (0, 0), (-1, -1), colors.darkslategray), # 设置表格内文字颜色
('GRID', (0, 0), (-1, -1), 0.5, colors.grey), # 设置表格框线为grey色,线宽为0.5
# ('SPAN', (0, 1), (0, 2)), # 合并第一列二三行
# ('SPAN', (0, 3), (0, 4)), # 合并第一列三四行
# ('SPAN', (0, 5), (0, 6)), # 合并第一列五六行
# ('SPAN', (0, 7), (0, 8)), # 合并第一列五六行
]
# table = Table(args, colWidths=col_width, style=style)
table = Table(args, style=style)
return table
# 创建图表
@staticmethod
def draw_bar(bar_data: list, ax: list, items: list):
drawing = Drawing(500, 250)
bc = VerticalBarChart()
bc.x = 45 # 整个图表的x坐标
bc.y = 45 # 整个图表的y坐标
bc.height = 200 # 图表的高度
bc.width = 350 # 图表的宽度
bc.data = bar_data
bc.strokeColor = colors.black # 顶部和右边轴线的颜色
bc.valueAxis.valueMin = 5000 # 设置y坐标的最小值
bc.valueAxis.valueMax = 26000 # 设置y坐标的最大值
bc.valueAxis.valueStep = 2000 # 设置y坐标的步长
bc.categoryAxis.labels.dx = 2
bc.categoryAxis.labels.dy = -8
bc.categoryAxis.labels.angle = 20
bc.categoryAxis.categoryNames = ax
# 图示
leg = Legend()
leg.fontName = 'SimSun'
leg.alignment = 'right'
leg.boxAnchor = 'ne'
leg.x = 475 # 图例的x坐标
leg.y = 240
leg.dxTextSpace = 10
leg.columnMaximum = 3
leg.colorNamePairs = items
drawing.add(leg)
drawing.add(bc)
return drawing
# 绘制图片
@staticmethod
def draw_img(path):
img = Image(path) # 读取指定路径下的图片
img.drawWidth = 0.8 * cm # 设置图片的宽度
img.drawHeight = 0.8 * cm # 设置图片的高度
img.hAlign = 0
return img
def get_trajectory_image(path, width=400):
img = utils.ImageReader(path)
iw, ih = img.getSize()
aspect = ih / float(iw)
# 按照图片限宽锁定纵横比之后计算高度
height = (width * aspect)
# 如果高度过高,报告可能超出此页,则限高300,宽度按比例调整
HEIGHT_LIMIT = 300
if height > HEIGHT_LIMIT:
height = HEIGHT_LIMIT
width = height / aspect
return Image(path, width=width, height=height)
def report_generate(reportDict, reportPdf, trackPath):
# with open(f'{reportJson}', 'r', encoding='utf-8') as f:
# data = json.load(f)
styles = getSampleStyleSheet()
style_arial = styles['Normal']
style_arial.fontName = 'Arial' # 设置字体为Arial
style_simsun = styles['Normal']
style_simsun.fontName = 'SimSun' # 设置字体为SimSun
data = reportDict # dict
# 创建PDF文档
# 提取数据
name = data["name"]
test_mileage = data["testMileage"]
test_duration = data["testDuration"]
algorithm_result_description = data["algorithmResultDescription"]
# dimension description
safe_description = data["details"]["safe"]["description"]
comfort_description = data["details"]["comfort"]["description"]
accurate_description = data["details"]["accurate"]["description"]
efficient_description = data["details"]["efficient"]["description"]
# score
collisionCount_score = data["details"]["safe"]["indexes"]["collisionCount"]["score"]
collisionRisk_score = data["details"]["safe"]["indexes"]["collisionRisk"]["score"]
collisionSeverity_score = data["details"]["safe"]["indexes"]["collisionSeverity"]["score"]
overSpeed_score = data["details"]["safe"]["indexes"]["overSpeed"]["score"]
comfortzigzag_score = data["details"]["comfort"]["indexes"]["zigzag"]["score"]
comfortcadence_score = data["details"]["comfort"]["indexes"]["cadence"]["score"]
comfortsharpchangeofspeed_score = data["details"]["comfort"]["indexes"]["sharpchangeofspeed"]["score"]
comfortSpeed_score = data["details"]["comfort"]["indexes"]["comfortSpeed"]["score"]
positionError_score = data["details"]["accurate"]["indexes"]["positionError"]["score"]
executeAccurateError_score = data["details"]["accurate"]["indexes"]["executeAccurateError"]["score"]
efficientDrive_score = data["details"]["efficient"]["indexes"]["efficientDrive"]["score"]
averageSpeed_score = data["details"]["efficient"]["indexes"]["averageSpeed"]["score"]
stopDuration_score = data["details"]["efficient"]["indexes"]["obstaclestopDuration"]["score"]
# metric description
collisionCount_description = data["details"]["safe"]["indexes"]["collisionCount"]["description"]
collisionRisk_description = data["details"]["safe"]["indexes"]["collisionRisk"]["description"]
collisionSeverity_description = data["details"]["safe"]["indexes"]["collisionSeverity"]["description"]
overSpeed_description = data["details"]["safe"]["indexes"]["overSpeed"]["description"]
comfortzigzag_description = data["details"]["comfort"]["indexes"]["zigzag"]["description"]
comfortcadence_description = data["details"]["comfort"]["indexes"]["cadence"]["description"]
comfortsharpchangeofspeed_description = data["details"]["comfort"]["indexes"]["sharpchangeofspeed"]["description"]
comfortSpeed_description = data["details"]["comfort"]["indexes"]["comfortSpeed"]["description"]
positionError_description = data["details"]["accurate"]["indexes"]["positionError"]["description"]
executeAccurateError_description = data["details"]["accurate"]["indexes"]["executeAccurateError"]["description"]
efficientDrive_description = data["details"]["efficient"]["indexes"]["efficientDrive"]["description"]
averageSpeed_description = data["details"]["efficient"]["indexes"]["averageSpeed"]["description"]
stopDuration_description = data["details"]["efficient"]["indexes"]["obstaclestopDuration"]["description"]
# score level instruction
level_instruction_parts = [
"说明:优秀("
"90≤"
"分值"
"≤100"
"),良好("
"80≤"
"分值"
"<90"
"),一般("
"60≤"
"分值"
"<80"
"),较差("
"0≤"
"分值"
"<60"
")。"
]
level_instruction = " ".join(
[part for part in level_instruction_parts]
)
# 创建表格数据
table_data = [
("评价维度", "评价指标", "基准值", "权重", "分数", "指标描述"),
("安全性", "碰撞次数", "0次", "50%", "{:.02f}".format(collisionCount_score), collisionCount_description),
("安全性", "碰撞风险概率", "10%", "5%", "{:.02f}".format(collisionRisk_score), collisionRisk_description),
("安全性", "碰撞严重程度", "10%", "5%", "{:.02f}".format(collisionSeverity_score), collisionSeverity_description),
("安全性", "超速比例", "0%", "5%", "{:.02f}".format(overSpeed_score), overSpeed_description),
("平顺性", "画龙", "0次", "5%", "{:.02f}".format(comfortzigzag_score), comfortzigzag_description),
("平顺性", "顿挫", "0次", "5%", "{:.02f}".format(comfortcadence_score), comfortcadence_description),
("平顺性", "急加减速", "0次", "5%", "{:.02f}".format(comfortsharpchangeofspeed_score), comfortsharpchangeofspeed_description),
("平顺性", "指令跳变次数", "0次", "5%", "{:.02f}".format(comfortSpeed_score), comfortSpeed_description),
("准确性", "位置偏移误差", "0.3m", "5%", "{:.02f}".format(positionError_score), positionError_description),
("准确性", "任务执行错误次数", "0次", "5%", "{:.02f}".format(executeAccurateError_score), executeAccurateError_description),
("高效性", "无障碍物停止", "0次", "5%", "{:.02f}".format(efficientDrive_score), efficientDrive_description),
("高效性", "有障碍物停止", "5s", "5%", "{:.02f}".format(stopDuration_score), stopDuration_description)
]
# level_instruction = " ".join(
# [str(part) if not isinstance(part, Paragraph) else part for part in level_instruction_parts])
# 创建内容对应的空列表
content = list()
content.append(ReportPDF.draw_img('Pji.png'))
content.append(ReportPDF.draw_title('朴津室外AMR算法评价报告'))
# content.append(ReportPDF.draw_line_feed("换行"))
# content.append(ReportPDF.draw_line_feed("换行"))
### 报告内容填充
# 综述
base_info = f"数据包: {name}, 行驶时长: {test_duration}, 行驶里程: {test_mileage}"
content.append(ReportPDF.draw_text(base_info))
content.append(ReportPDF.draw_text(algorithm_result_description))
content.append(ReportPDF.draw_text(safe_description))
content.append(ReportPDF.draw_text(comfort_description))
content.append(ReportPDF.draw_text(accurate_description))
content.append(ReportPDF.draw_text(efficient_description))
# for item in level_instruction_parts:
# content.append(item)
content.append(ReportPDF.draw_text(level_instruction))
# 表格
content.append(ReportPDF.draw_line_feed("换行"))
content.append(ReportPDF.draw_table(*table_data))
content.append(ReportPDF.draw_line_feed("换行"))
# 图片
# 添加图片到PDF文档
content.append(PageBreak())
# content.append(ReportPDF.draw_line_feed("换行"))
# content.append(ReportPDF.draw_line_feed("换行"))
# content.append(ReportPDF.draw_line_feed("换行"))
# content.append(ReportPDF.draw_line_feed("换行"))
# content.append(ReportPDF.draw_line_feed("换行"))
for key, value in data["graphPath"].items():
# 特判轨迹图片情况
if "track.png" in value: # 评价工具生成轨迹图片
trackPath = value
continue
if "trajectory.png" in value: # ros生成带地图轨迹图片
continue
content.append(ReportPDF.draw_text(key, 'Heading5'))
content.append(Image(value, width=480, height=120))
# content.append(ReportPDF.draw_line_feed("换行"))
content.append(ReportPDF.draw_line_feed("换行"))
# content.append(PageBreak())
# 添加轨迹图片
content.append(PageBreak())
content.append(ReportPDF.draw_text("轨迹", 'Heading5'))
content.append(get_trajectory_image(trackPath))
if "track.png" in trackPath:
content.append(
ReportPDF.draw_text(" 说明:轨迹起点为黄色点,随后颜色逐渐加深。", 'Normal', 8))
else:
content.append(
ReportPDF.draw_text(" 说明:轨迹起点为绿色点,终点为红色点。", 'Normal', 8))
# content.append(ReportPDF.draw_line_feed("换行"))
# content.append(ReportPDF.draw_line_feed("换行"))
# 创建PDF文档
pdf_file = reportPdf
doc = SimpleDocTemplate(pdf_file, pagesize=A4)
# 构建文档
doc.build(content)
print("PDF报告已生成!")
if __name__ == '__main__':
# reportJson = './report.json'
reportJson = '/home/server/桌面/virtualEnv_yzh/virtual_pji/pji_outdoor_robot_evaluate_real-0808/task/test_0807-1/report.json'
reportPdf = "./report0809.pdf"
trackPath = './track.png'
with open(reportJson, 'r', encoding='utf-8') as f:
reportJson = json.load(f)
report_generate(reportJson, reportPdf, trackPath)
print('over')