|
|
@@ -3249,16 +3249,106 @@ class PDFReportCreator(ReportCreator):
|
|
3249
|
3249
|
ReportCreator.__init__(self)
|
|
3250
|
3250
|
self.pdf = None
|
|
3251
|
3251
|
self.output_path = None
|
|
|
3252
|
+ # Define color schemes for better visual appeal
|
|
|
3253
|
+ self.colors = {
|
|
|
3254
|
+ 'header': (41, 128, 185), # Blue
|
|
|
3255
|
+ 'text': (0, 0, 0), # Black
|
|
|
3256
|
+ 'table_header': (52, 152, 219), # Light blue
|
|
|
3257
|
+ 'table_alt': (245, 245, 245) # Light gray
|
|
|
3258
|
+ }
|
|
|
3259
|
+
|
|
|
3260
|
+ def _set_color(self, color_type='text', fill=False):
|
|
|
3261
|
+ """Set text or fill color using predefined color scheme."""
|
|
|
3262
|
+ if color_type in self.colors:
|
|
|
3263
|
+ r, g, b = self.colors[color_type]
|
|
|
3264
|
+ if fill:
|
|
|
3265
|
+ self.pdf.set_fill_color(r, g, b)
|
|
|
3266
|
+ else:
|
|
|
3267
|
+ self.pdf.set_text_color(r, g, b)
|
|
|
3268
|
+
|
|
|
3269
|
+ def _add_section_header(self, title, level=1):
|
|
|
3270
|
+ """Add a standardized section header with consistent formatting."""
|
|
|
3271
|
+ # Add some space before header
|
|
|
3272
|
+ self.pdf.ln(h=10)
|
|
|
3273
|
+
|
|
|
3274
|
+ # Set header color and font
|
|
|
3275
|
+ self._set_color('header')
|
|
|
3276
|
+ if level == 1:
|
|
|
3277
|
+ self.pdf.set_font('helvetica', 'B', 20)
|
|
|
3278
|
+ height = 15
|
|
|
3279
|
+ elif level == 2:
|
|
|
3280
|
+ self.pdf.set_font('helvetica', 'B', 16)
|
|
|
3281
|
+ height = 12
|
|
|
3282
|
+ else:
|
|
|
3283
|
+ self.pdf.set_font('helvetica', 'B', 14)
|
|
|
3284
|
+ height = 10
|
|
|
3285
|
+
|
|
|
3286
|
+ # Add the header
|
|
|
3287
|
+ self.pdf.cell(0, height, title, 0, new_x=XPos.LMARGIN, new_y=YPos.NEXT, align='L')
|
|
|
3288
|
+
|
|
|
3289
|
+ # Reset color to text
|
|
|
3290
|
+ self._set_color('text')
|
|
|
3291
|
+ self.pdf.ln(h=5) # Small gap after header
|
|
|
3292
|
+
|
|
|
3293
|
+ def _create_table_header(self, headers, widths=None, font_size=9):
|
|
|
3294
|
+ """Create a standardized table header with consistent formatting."""
|
|
|
3295
|
+ if widths is None:
|
|
|
3296
|
+ # Auto-calculate widths if not provided
|
|
|
3297
|
+ total_width = 180 # Reasonable default
|
|
|
3298
|
+ widths = [total_width // len(headers)] * len(headers)
|
|
|
3299
|
+
|
|
|
3300
|
+ # Set header styling
|
|
|
3301
|
+ self._set_color('table_header')
|
|
|
3302
|
+ self._set_color('table_header', fill=True)
|
|
|
3303
|
+ self.pdf.set_font('helvetica', 'B', font_size)
|
|
|
3304
|
+
|
|
|
3305
|
+ # Create header cells
|
|
|
3306
|
+ for i, (header, width) in enumerate(zip(headers, widths)):
|
|
|
3307
|
+ is_last = (i == len(headers) - 1)
|
|
|
3308
|
+ new_x = XPos.LMARGIN if is_last else XPos.RIGHT
|
|
|
3309
|
+ new_y = YPos.NEXT if is_last else YPos.TOP
|
|
|
3310
|
+
|
|
|
3311
|
+ self.pdf.cell(width, 8, str(header), 1,
|
|
|
3312
|
+ new_x=new_x, new_y=new_y, align='C', fill=True)
|
|
|
3313
|
+
|
|
|
3314
|
+ # Reset styling for table content
|
|
|
3315
|
+ self._set_color('text')
|
|
|
3316
|
+ self.pdf.set_font('helvetica', '', font_size - 1)
|
|
|
3317
|
+
|
|
|
3318
|
+ def _create_table_row(self, values, widths, alternate_row=False, font_size=8):
|
|
|
3319
|
+ """Create a table row with optional alternating background."""
|
|
|
3320
|
+ if alternate_row:
|
|
|
3321
|
+ self._set_color('table_alt', fill=True)
|
|
|
3322
|
+
|
|
|
3323
|
+ for i, (value, width) in enumerate(zip(values, widths)):
|
|
|
3324
|
+ is_last = (i == len(values) - 1)
|
|
|
3325
|
+ new_x = XPos.LMARGIN if is_last else XPos.RIGHT
|
|
|
3326
|
+ new_y = YPos.NEXT if is_last else YPos.TOP
|
|
|
3327
|
+
|
|
|
3328
|
+ # Truncate long values to fit
|
|
|
3329
|
+ str_value = str(value)
|
|
|
3330
|
+ if len(str_value) > width // 3: # Rough character width estimation
|
|
|
3331
|
+ str_value = str_value[:width//3-2] + '...'
|
|
|
3332
|
+
|
|
|
3333
|
+ self.pdf.cell(width, 6, str_value, 1,
|
|
|
3334
|
+ new_x=new_x, new_y=new_y, align='C', fill=alternate_row)
|
|
3252
|
3335
|
|
|
3253
|
3336
|
def create(self, data, path):
|
|
3254
|
3337
|
ReportCreator.create(self, data, path)
|
|
3255
|
3338
|
self.title = data.projectname
|
|
3256
|
3339
|
self.output_path = path
|
|
3257
|
3340
|
|
|
3258
|
|
- # Initialize PDF document
|
|
|
3341
|
+ # Initialize PDF document with fpdf2 features
|
|
3259
|
3342
|
self.pdf = FPDF()
|
|
3260
|
3343
|
self.pdf.set_auto_page_break(auto=True, margin=15)
|
|
3261
|
3344
|
|
|
|
3345
|
+ # Set metadata for better PDF properties
|
|
|
3346
|
+ self.pdf.set_title(f"GitStats Report - {data.projectname}")
|
|
|
3347
|
+ self.pdf.set_author("GitStats")
|
|
|
3348
|
+ self.pdf.set_subject(f"Git repository analysis for {data.projectname}")
|
|
|
3349
|
+ self.pdf.set_creator("GitStats with fpdf2")
|
|
|
3350
|
+ self.pdf.set_keywords("git,statistics,analysis,repository")
|
|
|
3351
|
+
|
|
3262
|
3352
|
# Create all pages (tabs)
|
|
3263
|
3353
|
self._create_title_page(data)
|
|
3264
|
3354
|
self._create_general_page(data)
|
|
|
@@ -3270,13 +3360,24 @@ class PDFReportCreator(ReportCreator):
|
|
3270
|
3360
|
self._create_tags_page(data)
|
|
3271
|
3361
|
self._create_branches_page(data)
|
|
3272
|
3362
|
|
|
3273
|
|
- # Save PDF
|
|
|
3363
|
+ # Save PDF with fpdf2's enhanced output method
|
|
3274
|
3364
|
pdf_path = os.path.join(path, f"gitstats_{data.projectname.replace(' ', '_')}.pdf")
|
|
3275
|
|
- self.pdf.output(pdf_path)
|
|
3276
|
|
- print(f"PDF report saved to: {pdf_path}")
|
|
|
3365
|
+
|
|
|
3366
|
+ # Use fpdf2's output method with proper file handling
|
|
|
3367
|
+ try:
|
|
|
3368
|
+ self.pdf.output(pdf_path)
|
|
|
3369
|
+ print(f"PDF report saved to: {pdf_path}")
|
|
|
3370
|
+ # Verify file was created and has content
|
|
|
3371
|
+ if os.path.exists(pdf_path) and os.path.getsize(pdf_path) > 0:
|
|
|
3372
|
+ print(f"PDF file size: {os.path.getsize(pdf_path)} bytes")
|
|
|
3373
|
+ else:
|
|
|
3374
|
+ print("Warning: PDF file was not created properly")
|
|
|
3375
|
+ except Exception as e:
|
|
|
3376
|
+ print(f"Error saving PDF: {e}")
|
|
|
3377
|
+ raise
|
|
3277
|
3378
|
|
|
3278
|
3379
|
def _add_chart_if_exists(self, chart_filename, width=None, height=None):
|
|
3279
|
|
- """Add a chart image to the PDF if it exists."""
|
|
|
3380
|
+ """Add a chart image to the PDF if it exists, with improved fpdf2 handling."""
|
|
3280
|
3381
|
chart_path = os.path.join(self.output_path, chart_filename)
|
|
3281
|
3382
|
if os.path.exists(chart_path):
|
|
3282
|
3383
|
try:
|
|
|
@@ -3284,22 +3385,29 @@ class PDFReportCreator(ReportCreator):
|
|
3284
|
3385
|
x = self.pdf.get_x()
|
|
3285
|
3386
|
y = self.pdf.get_y()
|
|
3286
|
3387
|
|
|
3287
|
|
- # Calculate dimensions
|
|
|
3388
|
+ # Calculate dimensions with better defaults
|
|
3288
|
3389
|
if width is None:
|
|
3289
|
3390
|
width = 150 # Default width
|
|
3290
|
3391
|
if height is None:
|
|
3291
|
3392
|
height = 80 # Default height
|
|
3292
|
3393
|
|
|
3293
|
|
- # Check if there's enough space, if not add page break
|
|
3294
|
|
- if y + height > 280: # 280 is roughly page height minus margin
|
|
|
3394
|
+ # Get page dimensions for better space calculation
|
|
|
3395
|
+ page_width = self.pdf.w
|
|
|
3396
|
+ page_height = self.pdf.h
|
|
|
3397
|
+ margin = 15 # Same as auto_page_break margin
|
|
|
3398
|
+
|
|
|
3399
|
+ # Check if there's enough space on current page
|
|
|
3400
|
+ if y + height > (page_height - margin):
|
|
3295
|
3401
|
self.pdf.add_page()
|
|
|
3402
|
+ x = self.pdf.get_x()
|
|
3296
|
3403
|
y = self.pdf.get_y()
|
|
3297
|
3404
|
|
|
3298
|
|
- # Add image
|
|
3299
|
|
- self.pdf.image(chart_path, x, y, width, height)
|
|
|
3405
|
+ # Add image with fpdf2's enhanced image handling
|
|
|
3406
|
+ # fpdf2 automatically handles different image formats
|
|
|
3407
|
+ self.pdf.image(chart_path, x=x, y=y, w=width, h=height)
|
|
3300
|
3408
|
|
|
3301
|
|
- # Move cursor below image
|
|
3302
|
|
- self.pdf.set_y(y + height + 5)
|
|
|
3409
|
+ # Move cursor below image with better spacing
|
|
|
3410
|
+ self.pdf.set_y(y + height + 8) # Increased spacing for better layout
|
|
3303
|
3411
|
|
|
3304
|
3412
|
return True
|
|
3305
|
3413
|
except Exception as e:
|