Преглед изворни кода

added export to pdf function and .gitignore

lechibang-1512 пре 2 месеци
родитељ
комит
aad53f01e2
2 измењених фајлова са 613 додато и 4 уклоњено
  1. 65
    0
      .gitignore
  2. 548
    4
      gitstats

+ 65
- 0
.gitignore Прегледај датотеку

@@ -0,0 +1,65 @@
1
+# Python
2
+__pycache__/
3
+*.py[cod]
4
+*$py.class
5
+
6
+# Virtual environments
7
+/.venv/
8
+venv/
9
+env/
10
+ENV/
11
+.env/
12
+
13
+# Distribution / packaging
14
+build/
15
+dist/
16
+*.egg-info/
17
+.eggs/
18
+pip-wheel-metadata/
19
+wheelhouse/
20
+
21
+# Installer logs
22
+pip-log.txt
23
+pip-delete-this-directory.txt
24
+
25
+# Unit test / coverage
26
+htmlcov/
27
+.coverage
28
+.coverage.*
29
+.cache
30
+.pytest_cache/
31
+
32
+# Type checkers / caches
33
+.mypy_cache/
34
+.pyre/
35
+
36
+# IDEs and editors
37
+.vscode/
38
+.idea/
39
+*.sublime-*
40
+*.code-workspace
41
+
42
+# Jupyter
43
+.ipynb_checkpoints
44
+
45
+# OS generated files
46
+.DS_Store
47
+Thumbs.db
48
+
49
+# Logs and local data
50
+*.log
51
+*.sqlite3
52
+
53
+# Environment & secrets
54
+.env
55
+.env.local
56
+.env.*.local
57
+secrets.json
58
+
59
+# Compiled extensions
60
+*.so
61
+*.dylib
62
+
63
+# Misc
64
+*.egg
65
+

+ 548
- 4
gitstats Прегледај датотеку

@@ -13,6 +13,7 @@ import subprocess
13 13
 import sys
14 14
 import time
15 15
 import zlib
16
+from fpdf import FPDF
16 17
 
17 18
 if sys.version_info < (3, 6):
18 19
 	print("Python 3.6 or higher is required for gitstats", file=sys.stderr)
@@ -47,7 +48,8 @@ conf = {
47 48
 	'linear_linestats': 1,
48 49
 	'project_name': '',
49 50
 	'processes': 8,
50
-	'start_date': ''
51
+	'start_date': '',
52
+	'output_format': 'html'  # 'html', 'pdf', or 'both'
51 53
 }
52 54
 
53 55
 def getpipeoutput(cmds, quiet = False):
@@ -1419,6 +1421,524 @@ plot """
1419 1421
 </ul>
1420 1422
 </div>
1421 1423
 """)
1424
+
1425
+class PDFReportCreator(ReportCreator):
1426
+	"""Creates PDF reports using fpdf2 library with embedded charts and tab-based structure."""
1427
+	
1428
+	def __init__(self):
1429
+		ReportCreator.__init__(self)
1430
+		self.pdf = None
1431
+		self.output_path = None
1432
+	
1433
+	def create(self, data, path):
1434
+		ReportCreator.create(self, data, path)
1435
+		self.title = data.projectname
1436
+		self.output_path = path
1437
+		
1438
+		# Initialize PDF document
1439
+		self.pdf = FPDF()
1440
+		self.pdf.set_auto_page_break(auto=True, margin=15)
1441
+		
1442
+		# Create all pages (tabs)
1443
+		self._create_title_page(data)
1444
+		self._create_general_page(data)
1445
+		self._create_activity_page(data)
1446
+		self._create_authors_page(data)
1447
+		self._create_files_page(data)
1448
+		self._create_lines_page(data)
1449
+		self._create_tags_page(data)
1450
+		
1451
+		# Save PDF
1452
+		pdf_path = os.path.join(path, f"gitstats_{data.projectname.replace(' ', '_')}.pdf")
1453
+		self.pdf.output(pdf_path)
1454
+		print(f"PDF report saved to: {pdf_path}")
1455
+	
1456
+	def _add_chart_if_exists(self, chart_filename, width=None, height=None):
1457
+		"""Add a chart image to the PDF if it exists."""
1458
+		chart_path = os.path.join(self.output_path, chart_filename)
1459
+		if os.path.exists(chart_path):
1460
+			try:
1461
+				# Get current position
1462
+				x = self.pdf.get_x()
1463
+				y = self.pdf.get_y()
1464
+				
1465
+				# Calculate dimensions
1466
+				if width is None:
1467
+					width = 150  # Default width
1468
+				if height is None:
1469
+					height = 80  # Default height
1470
+				
1471
+				# Check if there's enough space, if not add page break
1472
+				if y + height > 280:  # 280 is roughly page height minus margin
1473
+					self.pdf.add_page()
1474
+					y = self.pdf.get_y()
1475
+				
1476
+				# Add image
1477
+				self.pdf.image(chart_path, x, y, width, height)
1478
+				
1479
+				# Move cursor below image
1480
+				self.pdf.set_y(y + height + 5)
1481
+				
1482
+				return True
1483
+			except Exception as e:
1484
+				print(f"Warning: Could not add chart {chart_filename}: {e}")
1485
+				return False
1486
+		return False
1487
+	
1488
+	def _create_title_page(self, data):
1489
+		"""Create the title page of the PDF report."""
1490
+		self.pdf.add_page()
1491
+		self.pdf.set_font('Arial', 'B', 24)
1492
+		self.pdf.cell(0, 20, f'GitStats Report - {data.projectname}', 0, 1, 'C')
1493
+		
1494
+		self.pdf.ln(10)
1495
+		self.pdf.set_font('Arial', '', 12)
1496
+		format = '%Y-%m-%d %H:%M:%S'
1497
+		
1498
+		# Report generation info
1499
+		self.pdf.cell(0, 10, f'Generated: {datetime.datetime.now().strftime(format)}', 0, 1)
1500
+		self.pdf.cell(0, 10, f'Generator: GitStats (version {getversion()})', 0, 1)
1501
+		self.pdf.cell(0, 10, f'Git Version: {getgitversion()}', 0, 1)
1502
+		if getgnuplotversion():
1503
+			self.pdf.cell(0, 10, f'Gnuplot Version: {getgnuplotversion()}', 0, 1)
1504
+		
1505
+		self.pdf.ln(10)
1506
+		self.pdf.cell(0, 10, f'Report Period: {data.getFirstCommitDate().strftime(format)} to {data.getLastCommitDate().strftime(format)}', 0, 1)
1507
+		
1508
+		# Table of contents
1509
+		self.pdf.ln(15)
1510
+		self.pdf.set_font('Arial', 'B', 16)
1511
+		self.pdf.cell(0, 10, 'Table of Contents', 0, 1, 'L')
1512
+		
1513
+		self.pdf.set_font('Arial', '', 12)
1514
+		sections = [
1515
+			'1. General Statistics',
1516
+			'2. Activity Statistics', 
1517
+			'3. Authors Statistics',
1518
+			'4. Files Statistics',
1519
+			'5. Lines of Code Statistics',
1520
+			'6. Tags Statistics'
1521
+		]
1522
+		
1523
+		for section in sections:
1524
+			self.pdf.cell(0, 8, section, 0, 1, 'L')
1525
+	
1526
+	def _create_general_page(self, data):
1527
+		"""Create the general statistics page (mirrors index.html)."""
1528
+		self.pdf.add_page()
1529
+		self.pdf.set_font('Arial', 'B', 20)
1530
+		self.pdf.cell(0, 15, '1. General Statistics', 0, 1, 'L')
1531
+		
1532
+		self.pdf.set_font('Arial', '', 12)
1533
+		
1534
+		# Calculate basic stats
1535
+		total_commits = data.getTotalCommits()
1536
+		total_active_days = len(data.getActiveDays()) if hasattr(data, 'getActiveDays') else 0
1537
+		delta_days = data.getCommitDeltaDays() if hasattr(data, 'getCommitDeltaDays') else 0
1538
+		total_authors = data.getTotalAuthors()
1539
+		
1540
+		# General statistics (matching index.html exactly)
1541
+		stats = [
1542
+			('Project name', data.projectname),
1543
+			('Generated', datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')),
1544
+			('Report Period', f"{data.getFirstCommitDate().strftime('%Y-%m-%d %H:%M:%S')} to {data.getLastCommitDate().strftime('%Y-%m-%d %H:%M:%S')}"),
1545
+			('Age', f"{delta_days} days, {total_active_days} active days ({(100.0 * total_active_days / delta_days) if delta_days else 0.0:.2f}%)"),
1546
+			('Total Files', str(data.getTotalFiles())),
1547
+			('Total Lines of Code', f"{data.getTotalLOC()} ({data.total_lines_added} added, {data.total_lines_removed} removed)"),
1548
+			('Total Commits', f"{total_commits} (average {(float(total_commits) / total_active_days) if total_active_days else 0.0:.1f} commits per active day, {(float(total_commits) / delta_days) if delta_days else 0.0:.1f} per all days)"),
1549
+			('Authors', f"{total_authors} (average {(float(total_commits) / total_authors) if total_authors else 0.0:.1f} commits per author)")
1550
+		]
1551
+		
1552
+		# Display stats
1553
+		for label, value in stats:
1554
+			self.pdf.cell(50, 8, f"{label}:", 0, 0, 'L')
1555
+			self.pdf.cell(0, 8, str(value), 0, 1, 'L')
1556
+		
1557
+		self.pdf.ln(10)
1558
+	
1559
+	def _create_activity_page(self, data):
1560
+		"""Create the activity statistics page with charts (mirrors activity.html)."""
1561
+		self.pdf.add_page()
1562
+		self.pdf.set_font('Arial', 'B', 20)
1563
+		self.pdf.cell(0, 15, '2. Activity Statistics', 0, 1, 'L')
1564
+		
1565
+		# Weekly activity section
1566
+		self.pdf.set_font('Arial', 'B', 14)
1567
+		self.pdf.cell(0, 10, 'Weekly Activity', 0, 1, 'L')
1568
+		self.pdf.set_font('Arial', '', 10)
1569
+		self.pdf.cell(0, 6, 'Last 32 weeks activity (see chart below)', 0, 1, 'L')
1570
+		self.pdf.ln(5)
1571
+		
1572
+		# Hour of Day section
1573
+		self.pdf.set_font('Arial', 'B', 14)
1574
+		self.pdf.cell(0, 10, 'Hour of Day', 0, 1, 'L')
1575
+		
1576
+		self.pdf.set_font('Arial', '', 10)
1577
+		hour_of_day = data.getActivityByHourOfDay()
1578
+		total_commits = data.getTotalCommits()
1579
+		
1580
+		# Create hour of day table
1581
+		self.pdf.set_font('Arial', 'B', 8)
1582
+		self.pdf.cell(20, 6, 'Hour', 1, 0, 'C')
1583
+		for h in range(0, 24):
1584
+			self.pdf.cell(7, 6, str(h), 1, 0, 'C')
1585
+		self.pdf.ln()
1586
+		
1587
+		self.pdf.cell(20, 6, 'Commits', 1, 0, 'C')
1588
+		for h in range(0, 24):
1589
+			commits = hour_of_day.get(h, 0)
1590
+			self.pdf.cell(7, 6, str(commits), 1, 0, 'C')
1591
+		self.pdf.ln()
1592
+		
1593
+		self.pdf.cell(20, 6, '%', 1, 0, 'C')
1594
+		for h in range(0, 24):
1595
+			commits = hour_of_day.get(h, 0)
1596
+			percent = (100.0 * commits / total_commits) if total_commits else 0.0
1597
+			self.pdf.cell(7, 6, f"{percent:.1f}", 1, 0, 'C')
1598
+		self.pdf.ln(10)
1599
+		
1600
+		# Add hour of day chart
1601
+		self._add_chart_if_exists('hour_of_day.png', 180, 90)
1602
+		
1603
+		# Day of Week section
1604
+		self.pdf.set_font('Arial', 'B', 14)
1605
+		self.pdf.cell(0, 10, 'Day of Week', 0, 1, 'L')
1606
+		
1607
+		self.pdf.set_font('Arial', '', 10)
1608
+		day_of_week = data.getActivityByDayOfWeek()
1609
+		
1610
+		# Create day of week table
1611
+		self.pdf.set_font('Arial', 'B', 10)
1612
+		self.pdf.cell(30, 8, 'Day', 1, 0, 'C')
1613
+		self.pdf.cell(30, 8, 'Total (%)', 1, 1, 'C')
1614
+		
1615
+		self.pdf.set_font('Arial', '', 10)
1616
+		for d in range(0, 7):
1617
+			day_name = WEEKDAYS[d]
1618
+			commits = day_of_week.get(d, 0)
1619
+			percent = (100.0 * commits / total_commits) if total_commits else 0.0
1620
+			self.pdf.cell(30, 6, day_name, 1, 0, 'L')
1621
+			self.pdf.cell(30, 6, f"{commits} ({percent:.2f}%)", 1, 1, 'L')
1622
+		
1623
+		self.pdf.ln(5)
1624
+		self._add_chart_if_exists('day_of_week.png', 180, 90)
1625
+		
1626
+		# Month of Year section  
1627
+		if hasattr(data, 'activity_by_month_of_year'):
1628
+			self.pdf.set_font('Arial', 'B', 14) 
1629
+			self.pdf.cell(0, 10, 'Month of Year', 0, 1, 'L')
1630
+			
1631
+			self.pdf.set_font('Arial', 'B', 10)
1632
+			self.pdf.cell(30, 8, 'Month', 1, 0, 'C')
1633
+			self.pdf.cell(40, 8, 'Commits (%)', 1, 1, 'C')
1634
+			
1635
+			self.pdf.set_font('Arial', '', 10)
1636
+			for mm in range(1, 13):
1637
+				commits = data.activity_by_month_of_year.get(mm, 0)
1638
+				percent = (100.0 * commits / total_commits) if total_commits else 0.0
1639
+				self.pdf.cell(30, 6, str(mm), 1, 0, 'C')
1640
+				self.pdf.cell(40, 6, f"{commits} ({percent:.2f} %)", 1, 1, 'L')
1641
+			
1642
+			self.pdf.ln(5)
1643
+			self._add_chart_if_exists('month_of_year.png', 180, 90)
1644
+		
1645
+		# Add page break for next major chart
1646
+		if self.pdf.get_y() > 200:
1647
+			self.pdf.add_page()
1648
+		
1649
+		# Commits by year/month chart
1650
+		self.pdf.set_font('Arial', 'B', 14)
1651
+		self.pdf.cell(0, 10, 'Commits by Year/Month', 0, 1, 'L')
1652
+		self._add_chart_if_exists('commits_by_year_month.png', 180, 100)
1653
+		
1654
+		# Commits by year chart 
1655
+		self.pdf.set_font('Arial', 'B', 14)
1656
+		self.pdf.cell(0, 10, 'Commits by Year', 0, 1, 'L')
1657
+		self._add_chart_if_exists('commits_by_year.png', 180, 100)
1658
+	
1659
+	def _create_authors_page(self, data):
1660
+		"""Create the authors statistics page with charts (mirrors authors.html)."""
1661
+		self.pdf.add_page()
1662
+		self.pdf.set_font('Arial', 'B', 20)
1663
+		self.pdf.cell(0, 15, '3. Authors Statistics', 0, 1, 'L')
1664
+		
1665
+		# List of Authors table
1666
+		self.pdf.set_font('Arial', 'B', 14)
1667
+		self.pdf.cell(0, 10, 'List of Authors', 0, 1, 'L')
1668
+		
1669
+		authors = data.getAuthors(conf['max_authors'])
1670
+		
1671
+		# Table header
1672
+		self.pdf.set_font('Arial', 'B', 8)
1673
+		self.pdf.cell(35, 6, 'Author', 1, 0, 'C')
1674
+		self.pdf.cell(20, 6, 'Commits (%)', 1, 0, 'C')
1675
+		self.pdf.cell(15, 6, '+ lines', 1, 0, 'C')
1676
+		self.pdf.cell(15, 6, '- lines', 1, 0, 'C')
1677
+		self.pdf.cell(25, 6, 'First commit', 1, 0, 'C')
1678
+		self.pdf.cell(25, 6, 'Last commit', 1, 0, 'C')
1679
+		self.pdf.cell(20, 6, 'Age', 1, 0, 'C')
1680
+		self.pdf.cell(15, 6, 'Active days', 1, 1, 'C')
1681
+		
1682
+		# Table data
1683
+		self.pdf.set_font('Arial', '', 7)
1684
+		for author in authors[:20]:  # Top 20 authors
1685
+			info = data.getAuthorInfo(author)
1686
+			
1687
+			# Truncate long author names
1688
+			display_author = author[:18] + "..." if len(author) > 21 else author
1689
+			
1690
+			self.pdf.cell(35, 5, display_author, 1, 0, 'L')
1691
+			self.pdf.cell(20, 5, f"{info['commits']} ({info['commits_frac']:.1f}%)", 1, 0, 'C')
1692
+			self.pdf.cell(15, 5, str(info['lines_added']), 1, 0, 'C')
1693
+			self.pdf.cell(15, 5, str(info['lines_removed']), 1, 0, 'C')
1694
+			self.pdf.cell(25, 5, info['date_first'][:10], 1, 0, 'C')
1695
+			self.pdf.cell(25, 5, info['date_last'][:10], 1, 0, 'C')
1696
+			
1697
+			# Calculate age
1698
+			try:
1699
+				age_days = (datetime.datetime.strptime(info['date_last'][:10], '%Y-%m-%d') - 
1700
+						   datetime.datetime.strptime(info['date_first'][:10], '%Y-%m-%d')).days
1701
+				age_text = f"{age_days} days" if age_days > 0 else "1 day"
1702
+			except:
1703
+				age_text = "N/A"
1704
+			
1705
+			active_days = len(info.get('active_days', [0])) if 'active_days' in info else 1
1706
+			
1707
+			self.pdf.cell(20, 5, age_text[:12], 1, 0, 'C')
1708
+			self.pdf.cell(15, 5, str(active_days), 1, 1, 'C')
1709
+		
1710
+		self.pdf.ln(10)
1711
+		
1712
+		# Lines of code by author chart
1713
+		self.pdf.set_font('Arial', 'B', 14)
1714
+		self.pdf.cell(0, 10, 'Cumulated Added Lines of Code per Author', 0, 1, 'L')
1715
+		self._add_chart_if_exists('lines_of_code_by_author.png', 180, 110)
1716
+		
1717
+		# Commits per author chart
1718
+		self.pdf.set_font('Arial', 'B', 14)
1719
+		self.pdf.cell(0, 10, 'Commits per Author', 0, 1, 'L')
1720
+		self._add_chart_if_exists('commits_by_author.png', 180, 110)
1721
+		
1722
+		# Commits by domains chart
1723
+		self.pdf.set_font('Arial', 'B', 14)
1724
+		self.pdf.cell(0, 10, 'Commits by Domains', 0, 1, 'L')
1725
+		self._add_chart_if_exists('domains.png', 180, 100)
1726
+	
1727
+	def _create_files_page(self, data):
1728
+		"""Create the files statistics page with charts (mirrors files.html)."""
1729
+		self.pdf.add_page()
1730
+		self.pdf.set_font('Arial', 'B', 20)
1731
+		self.pdf.cell(0, 15, '4. Files Statistics', 0, 1, 'L')
1732
+		
1733
+		# Basic file stats
1734
+		total_files = data.getTotalFiles()
1735
+		total_loc = data.getTotalLOC()
1736
+		
1737
+		self.pdf.set_font('Arial', '', 12)
1738
+		stats = [
1739
+			('Total files', str(total_files)),
1740
+			('Total lines', str(total_loc)),
1741
+		]
1742
+		
1743
+		try:
1744
+			avg_size = float(data.getTotalSize()) / total_files if total_files else 0.0
1745
+			stats.append(('Average file size', f"{avg_size:.2f} bytes"))
1746
+		except (AttributeError, ZeroDivisionError):
1747
+			pass
1748
+		
1749
+		for label, value in stats:
1750
+			self.pdf.cell(50, 8, f"{label}:", 0, 0, 'L')
1751
+			self.pdf.cell(0, 8, str(value), 0, 1, 'L')
1752
+		
1753
+		self.pdf.ln(10)
1754
+		
1755
+		# File extensions
1756
+		if hasattr(data, 'extensions') and data.extensions:
1757
+			self.pdf.set_font('Arial', 'B', 14)
1758
+			self.pdf.cell(0, 10, 'File Extensions', 0, 1, 'L')
1759
+			
1760
+			# Table header
1761
+			self.pdf.set_font('Arial', 'B', 9)
1762
+			self.pdf.cell(25, 8, 'Extension', 1, 0, 'C')
1763
+			self.pdf.cell(20, 8, 'Files', 1, 0, 'C')
1764
+			self.pdf.cell(20, 8, '% Files', 1, 0, 'C')
1765
+			self.pdf.cell(25, 8, 'Lines', 1, 0, 'C')
1766
+			self.pdf.cell(20, 8, '% Lines', 1, 0, 'C')
1767
+			self.pdf.cell(25, 8, 'Lines/File', 1, 1, 'C')
1768
+			
1769
+			# Table data - show top extensions
1770
+			self.pdf.set_font('Arial', '', 8)
1771
+			sorted_extensions = sorted(data.extensions.items(), 
1772
+									 key=lambda x: x[1]['files'], reverse=True)[:15]
1773
+			
1774
+			for ext, ext_data in sorted_extensions:
1775
+				files = ext_data['files']
1776
+				lines = ext_data['lines']
1777
+				loc_percentage = (100.0 * lines / total_loc) if total_loc else 0.0
1778
+				files_percentage = (100.0 * files / total_files) if total_files else 0.0
1779
+				lines_per_file = (lines // files) if files else 0
1780
+				
1781
+				display_ext = ext if ext else '(no ext)'
1782
+				
1783
+				self.pdf.cell(25, 6, display_ext[:12], 1, 0, 'L')
1784
+				self.pdf.cell(20, 6, str(files), 1, 0, 'C')
1785
+				self.pdf.cell(20, 6, f"{files_percentage:.1f}%", 1, 0, 'C')
1786
+				self.pdf.cell(25, 6, str(lines), 1, 0, 'C')
1787
+				self.pdf.cell(20, 6, f"{loc_percentage:.1f}%", 1, 0, 'C')
1788
+				self.pdf.cell(25, 6, str(lines_per_file), 1, 1, 'C')
1789
+		
1790
+		self.pdf.ln(10)
1791
+		
1792
+		# Files by date chart
1793
+		self.pdf.set_font('Arial', 'B', 14)
1794
+		self.pdf.cell(0, 10, 'Files by Date', 0, 1, 'L')
1795
+		self._add_chart_if_exists('files_by_date.png', 180, 100)
1796
+	
1797
+	def _create_lines_page(self, data):
1798
+		"""Create the lines of code statistics page with charts (mirrors lines.html)."""
1799
+		self.pdf.add_page()
1800
+		self.pdf.set_font('Arial', 'B', 20)
1801
+		self.pdf.cell(0, 15, '5. Lines of Code Statistics', 0, 1, 'L')
1802
+		
1803
+		# Basic line stats
1804
+		self.pdf.set_font('Arial', '', 12)
1805
+		stats = [
1806
+			('Total lines', str(data.getTotalLOC())),
1807
+			('Lines added', str(data.total_lines_added)),
1808
+			('Lines removed', str(data.total_lines_removed)),
1809
+			('Net lines', str(data.total_lines_added - data.total_lines_removed)),
1810
+		]
1811
+		
1812
+		for label, value in stats:
1813
+			self.pdf.cell(50, 8, f"{label}:", 0, 0, 'L')
1814
+			self.pdf.cell(0, 8, str(value), 0, 1, 'L')
1815
+		
1816
+		self.pdf.ln(10)
1817
+		
1818
+		# Lines by year
1819
+		if hasattr(data, 'commits_by_year') and data.commits_by_year:
1820
+			self.pdf.set_font('Arial', 'B', 14)
1821
+			self.pdf.cell(0, 10, 'Activity by Year', 0, 1, 'L')
1822
+			
1823
+			# Table header
1824
+			self.pdf.set_font('Arial', 'B', 10)
1825
+			self.pdf.cell(25, 8, 'Year', 1, 0, 'C')
1826
+			self.pdf.cell(30, 8, 'Commits', 1, 0, 'C')
1827
+			self.pdf.cell(30, 8, '% of Total', 1, 0, 'C')
1828
+			self.pdf.cell(35, 8, 'Lines Added', 1, 0, 'C')
1829
+			self.pdf.cell(35, 8, 'Lines Removed', 1, 1, 'C')
1830
+			
1831
+			# Table data
1832
+			self.pdf.set_font('Arial', '', 9)
1833
+			total_commits = data.getTotalCommits()
1834
+			
1835
+			for yy in sorted(data.commits_by_year.keys(), reverse=True):
1836
+				commits = data.commits_by_year.get(yy, 0)
1837
+				percent = (100.0 * commits / total_commits) if total_commits else 0.0
1838
+				lines_added = data.lines_added_by_year.get(yy, 0) if hasattr(data, 'lines_added_by_year') else 0
1839
+				lines_removed = data.lines_removed_by_year.get(yy, 0) if hasattr(data, 'lines_removed_by_year') else 0
1840
+				
1841
+				self.pdf.cell(25, 6, str(yy), 1, 0, 'C')
1842
+				self.pdf.cell(30, 6, str(commits), 1, 0, 'C')
1843
+				self.pdf.cell(30, 6, f"{percent:.1f}%", 1, 0, 'C')
1844
+				self.pdf.cell(35, 6, str(lines_added), 1, 0, 'C')
1845
+				self.pdf.cell(35, 6, str(lines_removed), 1, 1, 'C')
1846
+		
1847
+		self.pdf.ln(10)
1848
+		
1849
+		# Lines of code chart
1850
+		self.pdf.set_font('Arial', 'B', 14)
1851
+		self.pdf.cell(0, 10, 'Lines of Code Over Time', 0, 1, 'L')
1852
+		self._add_chart_if_exists('lines_of_code.png', 180, 100)
1853
+	
1854
+	def _create_tags_page(self, data):
1855
+		"""Create the tags statistics page (mirrors tags.html)."""
1856
+		self.pdf.add_page()
1857
+		self.pdf.set_font('Arial', 'B', 20)
1858
+		self.pdf.cell(0, 15, '6. Tags Statistics', 0, 1, 'L')
1859
+		
1860
+		self.pdf.set_font('Arial', '', 12)
1861
+		
1862
+		if not hasattr(data, 'tags') or not data.tags:
1863
+			self.pdf.cell(0, 10, 'No tags found in repository.', 0, 1, 'L')
1864
+			return
1865
+		
1866
+		# Basic tag stats
1867
+		total_tags = len(data.tags)
1868
+		avg_commits_per_tag = (1.0 * data.getTotalCommits() / total_tags) if total_tags else 0.0
1869
+		
1870
+		stats = [
1871
+			('Total tags', str(total_tags)),
1872
+			('Average commits per tag', f"{avg_commits_per_tag:.2f}"),
1873
+		]
1874
+		
1875
+		for label, value in stats:
1876
+			self.pdf.cell(50, 8, f"{label}:", 0, 0, 'L')
1877
+			self.pdf.cell(0, 8, str(value), 0, 1, 'L')
1878
+		
1879
+		self.pdf.ln(10)
1880
+		
1881
+		# Tags table
1882
+		if hasattr(data, 'tags') and data.tags:
1883
+			self.pdf.set_font('Arial', 'B', 12)
1884
+			self.pdf.cell(0, 10, 'List of Tags', 0, 1, 'L')
1885
+			
1886
+			# Table header
1887
+			self.pdf.set_font('Arial', 'B', 10)
1888
+			self.pdf.cell(40, 8, 'Tag', 1, 0, 'C')
1889
+			self.pdf.cell(30, 8, 'Date', 1, 0, 'C')
1890
+			self.pdf.cell(30, 8, 'Commits', 1, 0, 'C')
1891
+			self.pdf.cell(50, 8, 'Author', 1, 1, 'C')
1892
+			
1893
+			# Table data
1894
+			self.pdf.set_font('Arial', '', 9)
1895
+			tag_list = sorted(data.tags.items(), key=lambda x: x[1]['date'], reverse=True)
1896
+			
1897
+			for tag, tag_data in tag_list[:20]:  # Show top 20 tags
1898
+				self.pdf.cell(40, 6, tag[:20], 1, 0, 'L')
1899
+				self.pdf.cell(30, 6, tag_data.get('date', 'N/A')[:10], 1, 0, 'C')
1900
+				self.pdf.cell(30, 6, str(tag_data.get('commits', 0)), 1, 0, 'C')
1901
+				author = tag_data.get('author', 'N/A')[:25]
1902
+				self.pdf.cell(50, 6, author, 1, 1, 'L')
1903
+		
1904
+		# Tags table
1905
+		self.pdf.set_font('Arial', 'B', 14)
1906
+		self.pdf.cell(0, 10, 'Recent Tags', 0, 1, 'L')
1907
+		
1908
+		# Table header
1909
+		self.pdf.set_font('Arial', 'B', 10)
1910
+		self.pdf.cell(40, 8, 'Tag Name', 1, 0, 'C')
1911
+		self.pdf.cell(30, 8, 'Date', 1, 0, 'C')
1912
+		self.pdf.cell(25, 8, 'Commits', 1, 0, 'C')
1913
+		self.pdf.cell(80, 8, 'Top Authors', 1, 1, 'C')
1914
+		
1915
+		# Sort tags by date (most recent first)
1916
+		tags_sorted_by_date_desc = list(map(lambda el : el[1], 
1917
+										  reversed(sorted(map(lambda el : (el[1]['date'], el[0]), 
1918
+														  data.tags.items())))))
1919
+		
1920
+		# Show up to 20 most recent tags
1921
+		self.pdf.set_font('Arial', '', 8)
1922
+		for tag in tags_sorted_by_date_desc[:20]:
1923
+			tag_info = data.tags[tag]
1924
+			
1925
+			# Get top authors for this tag
1926
+			if 'authors' in tag_info:
1927
+				authors = sorted(tag_info['authors'].items(), 
1928
+							   key=lambda x: x[1], reverse=True)[:3]
1929
+				author_list = ', '.join([f"{author}({commits})" for author, commits in authors])
1930
+			else:
1931
+				author_list = ''
1932
+			
1933
+			# Truncate long names
1934
+			display_tag = tag[:18] + "..." if len(tag) > 21 else tag
1935
+			display_authors = author_list[:35] + "..." if len(author_list) > 38 else author_list
1936
+			
1937
+			self.pdf.cell(40, 6, display_tag, 1, 0, 'L')
1938
+			self.pdf.cell(30, 6, tag_info['date'][:10], 1, 0, 'C')
1939
+			self.pdf.cell(25, 6, str(tag_info['commits']), 1, 0, 'C')
1940
+			self.pdf.cell(80, 6, display_authors, 1, 1, 'L')
1941
+		
1422 1942
 		
1423 1943
 def usage():
1424 1944
 	print("""
@@ -1427,6 +1947,16 @@ Usage: gitstats [options] <gitpath..> <outputpath>
1427 1947
 Options:
1428 1948
 -c key=value     Override configuration value
1429 1949
 
1950
+Available output formats (use -c output_format=<format>):
1951
+  html           Generate HTML report only (default)
1952
+  pdf            Generate PDF report only  
1953
+  both           Generate both HTML and PDF reports
1954
+
1955
+Examples:
1956
+  gitstats repo output                    # HTML report
1957
+  gitstats -c output_format=pdf repo output    # PDF report
1958
+  gitstats -c output_format=both repo output   # Both formats
1959
+
1430 1960
 Default config values:
1431 1961
 %s
1432 1962
 
@@ -1493,16 +2023,30 @@ class GitStats:
1493 2023
 		os.chdir(rundir)
1494 2024
 
1495 2025
 		print('Generating report...')
1496
-		report = HTMLReportCreator()
1497
-		report.create(data, outputpath)
2026
+		
2027
+		# Generate reports based on output_format configuration
2028
+		if conf['output_format'] in ('html', 'both'):
2029
+			print('Creating HTML report...')
2030
+			html_report = HTMLReportCreator()
2031
+			html_report.create(data, outputpath)
2032
+		
2033
+		if conf['output_format'] in ('pdf', 'both'):
2034
+			print('Creating PDF report...')
2035
+			pdf_report = PDFReportCreator()
2036
+			pdf_report.create(data, outputpath)
1498 2037
 
1499 2038
 		time_end = time.time()
1500 2039
 		exectime_internal = time_end - time_start
1501 2040
 		print('Execution time %.5f secs, %.5f secs (%.2f %%) in external commands)' % (exectime_internal, exectime_external, (100.0 * exectime_external) / exectime_internal))
2041
+		
1502 2042
 		if sys.stdin.isatty():
1503 2043
 			print('You may now run:')
1504 2044
 			print()
1505
-			print('   sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''"))
2045
+			if conf['output_format'] in ('html', 'both'):
2046
+				print('   sensible-browser \'%s\'' % os.path.join(outputpath, 'index.html').replace("'", "'\\''"))
2047
+			if conf['output_format'] in ('pdf', 'both'):
2048
+				pdf_filename = f"gitstats_{data.projectname.replace(' ', '_')}.pdf"
2049
+				print('   PDF report: \'%s\'' % os.path.join(outputpath, pdf_filename).replace("'", "'\\''"))
1506 2050
 			print()
1507 2051
 
1508 2052
 if __name__=='__main__':