|
|
|
|
|
import os |
|
|
import uuid |
|
|
from datetime import datetime |
|
|
from reportlab.lib.pagesizes import A4 |
|
|
from reportlab.lib.styles import ParagraphStyle, getSampleStyleSheet |
|
|
from reportlab.lib.enums import TA_CENTER, TA_RIGHT, TA_JUSTIFY, TA_LEFT |
|
|
from reportlab.lib import colors |
|
|
from reportlab.platypus import ( |
|
|
BaseDocTemplate, PageTemplate, Frame, Paragraph, Spacer, Table, TableStyle |
|
|
) |
|
|
from reportlab.pdfbase.ttfonts import TTFont |
|
|
from reportlab.pdfbase import pdfmetrics |
|
|
|
|
|
|
|
|
try: |
|
|
pdfmetrics.registerFont(TTFont('DejaVuSerif', '/usr/share/fonts/truetype/dejavu/DejaVuSerif.ttf')) |
|
|
BODY_FONT = 'DejaVuSerif' |
|
|
except Exception: |
|
|
BODY_FONT = 'Times-Roman' |
|
|
|
|
|
|
|
|
def _build_doc(filepath, title_text, tiles, counts, sections, matched_sources, footer_text): |
|
|
PAGE_WIDTH, PAGE_HEIGHT = A4 |
|
|
MARGIN = 36 |
|
|
usable_width = PAGE_WIDTH - 2 * MARGIN |
|
|
|
|
|
styles = getSampleStyleSheet() |
|
|
styles.add(ParagraphStyle(name='ReportTitle', fontName=BODY_FONT, fontSize=18, alignment=TA_CENTER, leading=22)) |
|
|
styles.add(ParagraphStyle(name='SmallRight', fontName=BODY_FONT, fontSize=9, alignment=TA_RIGHT, textColor=colors.HexColor("#555555"))) |
|
|
styles.add(ParagraphStyle(name='TileBig', fontName=BODY_FONT, fontSize=30, alignment=TA_CENTER, leading=32)) |
|
|
styles.add(ParagraphStyle(name='TileLabel', fontName=BODY_FONT, fontSize=10, alignment=TA_CENTER, textColor=colors.HexColor("#666666"))) |
|
|
styles.add(ParagraphStyle(name='SectionHeading', fontName=BODY_FONT, fontSize=13, spaceBefore=8, spaceAfter=4, leading=15)) |
|
|
styles.add(ParagraphStyle(name='Body', fontName=BODY_FONT, fontSize=11, leading=15, alignment=TA_JUSTIFY)) |
|
|
styles.add(ParagraphStyle(name='HighlightYellow', fontName=BODY_FONT, fontSize=11, leading=15, backColor=colors.HexColor("#fff3b0"), alignment=TA_JUSTIFY)) |
|
|
styles.add(ParagraphStyle(name='HighlightRed', fontName=BODY_FONT, fontSize=11, leading=15, backColor=colors.HexColor("#ffd6d6"), alignment=TA_JUSTIFY)) |
|
|
styles.add(ParagraphStyle(name='Footer', fontName=BODY_FONT, fontSize=9, alignment=TA_RIGHT, textColor=colors.HexColor("#666666"))) |
|
|
styles.add(ParagraphStyle(name='MatchedHeader', fontName=BODY_FONT, fontSize=12, leading=14, alignment=TA_LEFT, spaceBefore=6, spaceAfter=6)) |
|
|
|
|
|
def header_footer(canvas, doc): |
|
|
canvas.saveState() |
|
|
date_str = datetime.now().strftime("%d %B %Y, %H:%M") |
|
|
canvas.setFont(BODY_FONT, 9) |
|
|
canvas.setFillColor(colors.HexColor("#555555")) |
|
|
canvas.drawString(MARGIN, PAGE_HEIGHT - MARGIN + 8, f"Date: {date_str}") |
|
|
canvas.setFont(BODY_FONT, 16) |
|
|
canvas.setFillColor(colors.black) |
|
|
canvas.drawCentredString(PAGE_WIDTH / 2.0, PAGE_HEIGHT - MARGIN + 4, title_text) |
|
|
canvas.setFont(BODY_FONT, 9) |
|
|
canvas.setFillColor(colors.HexColor("#666666")) |
|
|
canvas.drawRightString(PAGE_WIDTH - MARGIN, MARGIN - 10, f"Page {doc.page}") |
|
|
canvas.restoreState() |
|
|
|
|
|
doc = BaseDocTemplate(filepath, pagesize=A4, |
|
|
leftMargin=MARGIN, rightMargin=MARGIN, |
|
|
topMargin=MARGIN, bottomMargin=MARGIN) |
|
|
frame = Frame(MARGIN, MARGIN, usable_width, PAGE_HEIGHT - 2 * MARGIN, id='normal') |
|
|
template = PageTemplate(id='report', frames=[frame], onPage=header_footer) |
|
|
doc.addPageTemplates([template]) |
|
|
|
|
|
story = [] |
|
|
|
|
|
|
|
|
tile_values = tiles |
|
|
tiles_data = [ |
|
|
[Paragraph(f"<b>{tile_values[i]['value']}</b>", styles['TileBig']) for i in range(4)], |
|
|
[Paragraph(tile_values[i]['label'], styles['TileLabel']) for i in range(4)] |
|
|
] |
|
|
tiles_table = Table(tiles_data, colWidths=[usable_width / 4.0] * 4, rowHeights=[46, 18]) |
|
|
tiles_table.setStyle(TableStyle([ |
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#f7f7f9")), |
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'), |
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), |
|
|
('BOX', (0, 0), (-1, -1), 0.6, colors.HexColor("#dddddd")), |
|
|
])) |
|
|
story.append(tiles_table) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
if counts: |
|
|
counts_table = Table([list(counts.keys()), list(counts.values())], |
|
|
colWidths=[usable_width / len(counts)] * len(counts)) |
|
|
counts_table.setStyle(TableStyle([ |
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#f4f6f7")), |
|
|
('ALIGN', (0, 0), (-1, -1), 'CENTER'), |
|
|
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor("#e6e6e6")), |
|
|
])) |
|
|
story.append(counts_table) |
|
|
story.append(Spacer(1, 12)) |
|
|
|
|
|
|
|
|
for sec in sections or []: |
|
|
if sec.get('heading'): |
|
|
story.append(Paragraph(sec['heading'], styles['SectionHeading'])) |
|
|
for para in sec.get('paragraphs', []): |
|
|
|
|
|
if isinstance(para, dict): |
|
|
text = para.get('text', '') |
|
|
hl = para.get('highlight') |
|
|
if hl == 'yellow': |
|
|
story.append(Paragraph(text, styles['HighlightYellow'])) |
|
|
elif hl == 'red': |
|
|
story.append(Paragraph(text, styles['HighlightRed'])) |
|
|
else: |
|
|
story.append(Paragraph(text, styles['Body'])) |
|
|
else: |
|
|
story.append(Paragraph(para, styles['Body'])) |
|
|
story.append(Spacer(1, 6)) |
|
|
|
|
|
story.append(Spacer(1, 10)) |
|
|
|
|
|
|
|
|
if matched_sources: |
|
|
story.append(Paragraph("Matched Sources", styles['MatchedHeader'])) |
|
|
ms_table_data = [["#", "Source Title", "URL", "Similarity"]] |
|
|
for i, ms in enumerate(matched_sources, start=1): |
|
|
title_par = Paragraph(ms.get('title', ''), styles['Body']) |
|
|
url_par = Paragraph(f'<link href="{ms.get("url", "")}">{ms.get("url", "")}</link>', styles['Body']) |
|
|
ms_table_data.append([str(i), title_par, url_par, ms.get('similarity', '')]) |
|
|
ms_table = Table(ms_table_data, colWidths=[30, usable_width * 0.35, usable_width * 0.45, usable_width * 0.15]) |
|
|
ms_table.setStyle(TableStyle([ |
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#f2f4f5")), |
|
|
('TEXTCOLOR', (0, 0), (-1, 0), colors.HexColor("#333333")), |
|
|
('ALIGN', (0, 0), (-1, 0), 'CENTER'), |
|
|
('VALIGN', (0, 0), (-1, -1), 'MIDDLE'), |
|
|
('BOX', (0, 0), (-1, -1), 0.6, colors.HexColor("#e0e0e0")), |
|
|
('INNERGRID', (0, 0), (-1, -1), 0.4, colors.HexColor("#efefef")), |
|
|
('LEFTPADDING', (1, 1), (1, -1), 6), |
|
|
('LEFTPADDING', (2, 1), (2, -1), 6), |
|
|
])) |
|
|
story.append(ms_table) |
|
|
story.append(Spacer(1, 14)) |
|
|
|
|
|
|
|
|
if footer_text: |
|
|
matched_table = Table( |
|
|
[[Paragraph("<b>Matched Source Overview</b>", styles['Body'])], |
|
|
[Paragraph(footer_text, styles['Body'])]], |
|
|
colWidths=[usable_width] |
|
|
) |
|
|
matched_table.setStyle(TableStyle([ |
|
|
('BACKGROUND', (0, 0), (-1, 0), colors.HexColor("#f7fafb")), |
|
|
('BOX', (0, 0), (-1, -1), 0.5, colors.HexColor("#e6e6e6")), |
|
|
('LEFTPADDING', (0, 0), (-1, -1), 8), |
|
|
('RIGHTPADDING', (0, 0), (-1, -1), 8), |
|
|
('TOPPADDING', (0, 0), (-1, -1), 6), |
|
|
('BOTTOMPADDING', (0, 0), (-1, -1), 6), |
|
|
])) |
|
|
story.append(matched_table) |
|
|
story.append(Spacer(1, 24)) |
|
|
|
|
|
story.append(Paragraph("Generated by TrueWrite Scan • https://gopalkrushnamahapatra-truewrite-scan.static.hf.space", styles['Footer'])) |
|
|
|
|
|
doc.build(story) |
|
|
|
|
|
|
|
|
def generate_report(report_type: str, out_dir: str = "/tmp", **kwargs) -> str: |
|
|
""" |
|
|
report_type: "ai" | "grammar" | "plagiarism" |
|
|
kwargs expected: |
|
|
- title_text: str |
|
|
- tiles: list of 4 dicts [{'value': '12%', 'label': 'Plagiarism'}, ...] |
|
|
- counts: dict {'Words': 950, ...} |
|
|
- sections: list [{'heading':'','paragraphs':[...]}] |
|
|
- matched_sources: list [{'title','url','similarity'}] |
|
|
- footer_text: str |
|
|
Returns: path to generated PDF |
|
|
""" |
|
|
os.makedirs(out_dir, exist_ok=True) |
|
|
filename = f"{report_type}_report_{uuid.uuid4().hex[:8]}.pdf" |
|
|
filepath = os.path.join(out_dir, filename) |
|
|
|
|
|
title_text = kwargs.get('title_text', "Report") |
|
|
tiles = kwargs.get('tiles') or [ |
|
|
{'value': '0%', 'label': 'Plagiarism'}, |
|
|
{'value': '0%', 'label': 'Exact Match'}, |
|
|
{'value': '0%', 'label': 'Partial Match'}, |
|
|
{'value': '100%', 'label': 'Unique'}, |
|
|
] |
|
|
counts = kwargs.get('counts') or {} |
|
|
sections = kwargs.get('sections') or [] |
|
|
matched_sources = kwargs.get('matched_sources') or [] |
|
|
footer_text = kwargs.get('footer_text') or '' |
|
|
|
|
|
_build_doc(filepath, title_text, tiles, counts, sections, matched_sources, footer_text) |
|
|
return filepath |
|
|
|