Repository: DenverCoder1/github-readme-streak-stats (php)

Issue:
Card is slightly higher than github-readme-stats cards. 
![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/49570123/2bb1419c-a708-4b8d-9867-39e4d76d297a)
The card is slightly higher than the github-readme-stats cards.

The first card in the image comes from: https://github.com/anuraghazra/github-readme-stats

You can see the difference at the bottom. 

My proposal is to add an option to set the card height. 

Pull request:
feat: Added card_height option
feat: create height option
- Created input for height option in form (src/demo/index.php)
- Update custom image height in process (src/card.php)

## Description

<!-- Please include a summary of the change and which issue is fixed. -->

Fixes #659 

### Type of change

<!-- Please delete options that are not relevant. -->

- [x] Bug fix (added a non-breaking change which fixes an issue)
- [x] New feature (added a non-breaking change which adds functionality)
- [ ] Updated documentation (updated the readme, templates, or other repo files)
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)

## How Has This Been Tested?

<!--
If you have changed a feature of the stats cards, please describe the tests you made to verify your changes.
Changes strictly related to documentation can skip this section.
-->

- [x] Tested locally with a valid username
- [x] Tested locally with an invalid username
- [ ] Ran tests with `composer test`
- [ ] Added or updated test cases to test new features

## Checklist:

- [ ] I have checked to make sure no other [pull requests](https://github.com/DenverCoder1/github-readme-streak-stats/pulls?q=is%3Apr+sort%3Aupdated-desc+) are open for this issue
- [x] The code is properly formatted and is consistent with the existing code style
- [x] I have commented my code, particularly in hard-to-understand areas
- [x] I have made corresponding changes to the documentation
- [ ] My changes generate no new warnings

## Screenshots

<!-- If you have updated the design or appearance, please include a screenshot of your changes. -->
![image](https://github.com/DenverCoder1/github-readme-streak-stats/assets/86786706/b5853ecf-3c83-42ef-a478-c03e932921b5)



Patch:
diff --git a/.github/workflows/phpunit-ci-coverage.yml b/.github/workflows/phpunit-ci-coverage.yml
index a8f2284..c059df7 100644
--- a/.github/workflows/phpunit-ci-coverage.yml
+++ b/.github/workflows/phpunit-ci-coverage.yml
@@ -20,7 +20,7 @@ jobs:
           php_extensions: intl
           php_version: ${{ env.PHP_VERSION }}
       - name: PHPUnit Tests
-        uses: php-actions/phpunit@v3
+        uses: php-actions/phpunit@v4
         with:
           php_extensions: intl
           php_version: ${{ env.PHP_VERSION }}
diff --git a/README.md b/README.md
index b6fc3b0..cd81a13 100644
--- a/README.md
+++ b/README.md
@@ -73,6 +73,7 @@ If the `theme` parameter is specified, any color customizations specified will b
 |       `exclude_days`       | List of days of the week to exclude from streaks |    Comma-separated list of day abbreviations (Sun, Mon, Tue, Wed, Thu, Fri, Sat) e.g. `Sun,Sat`    |
 |    `disable_animations`    |    Disable SVG animations (Default: `false`)     |                                         `true` or `false`                                          |
 |        `card_width`        |   Width of the card in pixels (Default: `495`)   |                        Positive integer, minimum width is 100px per column                         |
+|       `card_height`        |  Height of the card in pixels (Default: `195`)   |                             Positive integer, minimum height is 170px                              |
 | `hide_total_contributions` | Hide the total contributions (Default: `false`)  |                                         `true` or `false`                                          |
 |   `hide_current_streak`    |    Hide the current streak (Default: `false`)    |                                         `true` or `false`                                          |
 |   `hide_longest_streak`    |    Hide the longest streak (Default: `false`)    |                                         `true` or `false`                                          |
diff --git a/composer.lock b/composer.lock
index f61365d..75e613c 100644
--- a/composer.lock
+++ b/composer.lock
@@ -1028,16 +1028,16 @@
         },
         {
             "name": "phpunit/phpunit",
-            "version": "11.1.1",
+            "version": "11.1.3",
             "source": {
                 "type": "git",
                 "url": "https://github.com/sebastianbergmann/phpunit.git",
-                "reference": "816cb6070af901c0548aa5f18f2132fd8a6e4ade"
+                "reference": "d475be032238173ca3b0a516f5cc291d174708ae"
             },
             "dist": {
                 "type": "zip",
-                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/816cb6070af901c0548aa5f18f2132fd8a6e4ade",
-                "reference": "816cb6070af901c0548aa5f18f2132fd8a6e4ade",
+                "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/d475be032238173ca3b0a516f5cc291d174708ae",
+                "reference": "d475be032238173ca3b0a516f5cc291d174708ae",
                 "shasum": ""
             },
             "require": {
@@ -1108,7 +1108,7 @@
             "support": {
                 "issues": "https://github.com/sebastianbergmann/phpunit/issues",
                 "security": "https://github.com/sebastianbergmann/phpunit/security/policy",
-                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.1.1"
+                "source": "https://github.com/sebastianbergmann/phpunit/tree/11.1.3"
             },
             "funding": [
                 {
@@ -1124,7 +1124,7 @@
                     "type": "tidelift"
                 }
             ],
-            "time": "2024-04-06T06:20:21+00:00"
+            "time": "2024-04-24T06:34:25+00:00"
         },
         {
             "name": "sebastian/cli-parser",
diff --git a/src/card.php b/src/card.php
index d5ddc6d..3e4cf61 100644
--- a/src/card.php
+++ b/src/card.php
@@ -322,6 +322,19 @@ function getCardWidth(array $params, int $numColumns = 3): int
     return max($minimumWidth, intval($params["card_width"] ?? $defaultWidth));
 }
 
+/**
+ * Get the card height from params taking into account minimum and default values
+ *
+ * @param array<string,string> $params Request parameters
+ * @return int Card width
+ */
+function getCardHeight(array $params): int
+{
+    $defaultHeight = 195;
+    $minimumHeight = 170;
+    return max($minimumHeight, intval($params["card_height"] ?? $defaultHeight));
+}
+
 /**
  * Generate SVG output for a stats array
  *
@@ -364,7 +377,11 @@ function generateCard(array $stats, array $params = null): string
     $rectWidth = $cardWidth - 1;
     $columnWidth = $numColumns > 0 ? $cardWidth / $numColumns : 0;
 
-    // offsets for the bars between columns
+    $cardHeight = getCardHeight($params);
+    $rectHeight = $cardHeight - 1;
+    $heightOffset = ($cardHeight - 195) / 2;
+
+    // X offsets for the bars between columns
     $barOffsets = [-999, -999];
     for ($i = 0; $i < $numColumns - 1; $i++) {
         $barOffsets[$i] = $columnWidth * ($i + 1);
@@ -384,6 +401,22 @@ function generateCard(array $stats, array $params = null): string
     $currentStreakOffset = $showCurrentStreak ? $columnOffsets[$nextColumnIndex++] : -999;
     $longestStreakOffset = $showLongestStreak ? $columnOffsets[$nextColumnIndex++] : -999;
 
+    // Y offsets for the bars
+    $barHeightOffsets = [28 + $heightOffset / 2, 170 + $heightOffset];
+    // Y offsets for the numbers and dates
+    $longestStreakHeightOffset = $totalContributionsHeightOffset = [
+        48 + $heightOffset,
+        84 + $heightOffset,
+        114 + $heightOffset,
+    ];
+    $currentStreakHeightOffset = [
+        48 + $heightOffset,
+        108 + $heightOffset,
+        145 + $heightOffset,
+        71 + $heightOffset,
+        19.5 + $heightOffset,
+    ];
+
     // total contributions
     $totalContributions = $numFormatter->format($stats["totalContributions"]);
     $firstContribution = formatDate($stats["firstContribution"], $dateFormat, $localeCode);
@@ -440,7 +473,7 @@ function generateCard(array $stats, array $params = null): string
     }
 
     return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink'
-                style='isolation: isolate' viewBox='0 0 {$cardWidth} 195' width='{$cardWidth}px' height='195px' direction='{$direction}'>
+                style='isolation: isolate' viewBox='0 0 {$cardWidth} {$cardHeight}' width='{$cardWidth}px' height='{$cardHeight}px' direction='{$direction}'>
         <style>
             @keyframes currstreak {
                 0% { font-size: 3px; opacity: 0.2; }
@@ -454,39 +487,39 @@ function generateCard(array $stats, array $params = null): string
         </style>
         <defs>
             <clipPath id='outer_rectangle'>
-                <rect width='{$cardWidth}' height='195' rx='{$borderRadius}'/>
+                <rect width='{$cardWidth}' height='{$cardHeight}' rx='{$borderRadius}'/>
             </clipPath>
             <mask id='mask_out_ring_behind_fire'>
-                <rect width='{$cardWidth}' height='195' fill='white'/>
+                <rect width='{$cardWidth}' height='{$cardHeight}' fill='white'/>
                 <ellipse id='mask-ellipse' cx='{$currentStreakOffset}' cy='32' rx='13' ry='18' fill='black'/>
             </mask>
             {$theme["backgroundGradient"]}
         </defs>
         <g clip-path='url(#outer_rectangle)'>
             <g style='isolation: isolate'>
-                <rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='194'/>
+                <rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='{$rectHeight}'/>
             </g>
             <g style='isolation: isolate'>
-                <line x1='{$barOffsets[0]}' y1='28' x2='{$barOffsets[0]}' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
-                <line x1='{$barOffsets[1]}' y1='28' x2='{$barOffsets[1]}' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
+                <line x1='{$barOffsets[0]}' y1='{$barHeightOffsets[0]}' x2='{$barOffsets[0]}' y2='{$barHeightOffsets[1]}' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
+                <line x1='{$barOffsets[1]}' y1='$barHeightOffsets[0]' x2='{$barOffsets[1]}' y2='$barHeightOffsets[1]' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
             </g>
             <g style='isolation: isolate'>
                 <!-- Total Contributions big number -->
-                <g transform='translate({$totalContributionsOffset},48)'>
+                <g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[0]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideNums"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
                         {$totalContributions}
                     </text>
                 </g>
 
                 <!-- Total Contributions label -->
-                <g transform='translate({$totalContributionsOffset},84)'>
+                <g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[1]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideLabels"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.7s'>
                         {$totalContributionsText}
                     </text>
                 </g>
 
                 <!-- Total Contributions range -->
-                <g transform='translate({$totalContributionsOffset},114)'>
+                <g transform='translate({$totalContributionsOffset}, {$totalContributionsHeightOffset[2]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.8s'>
                         {$totalContributionsRange}
                     </text>
@@ -494,21 +527,21 @@ function generateCard(array $stats, array $params = null): string
             </g>
             <g style='isolation: isolate'>
                 <!-- Current Streak big number -->
-                <g transform='translate({$currentStreakOffset},48)'>
+                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[0]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["currStreakNum"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='animation: currstreak 0.6s linear forwards'>
                         {$currentStreak}
                     </text>
                 </g>
 
                 <!-- Current Streak label -->
-                <g transform='translate({$currentStreakOffset},108)'>
+                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[1]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["currStreakLabel"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
                         {$currentStreakText}
                     </text>
                 </g>
 
                 <!-- Current Streak range -->
-                <g transform='translate({$currentStreakOffset},145)'>
+                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[2]})'>
                     <text x='0' y='21' stroke-width='0' text-anchor='middle' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
                         {$currentStreakRange}
                     </text>
@@ -516,10 +549,10 @@ function generateCard(array $stats, array $params = null): string
 
                 <!-- Ring around number -->
                 <g mask='url(#mask_out_ring_behind_fire)'>
-                    <circle cx='{$currentStreakOffset}' cy='71' r='40' fill='none' stroke='{$theme["ring"]}' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>
+                    <circle cx='{$currentStreakOffset}' cy='{$currentStreakHeightOffset[3]}' r='40' fill='none' stroke='{$theme["ring"]}' stroke-width='5' style='opacity: 0; animation: fadein 0.5s linear forwards 0.4s'></circle>
                 </g>
                 <!-- Fire icon -->
-                <g transform='translate({$currentStreakOffset}, 19.5)' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
+                <g transform='translate({$currentStreakOffset}, {$currentStreakHeightOffset[4]})' stroke-opacity='0' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
                     <path d='M -12 -0.5 L 15 -0.5 L 15 23.5 L -12 23.5 L -12 -0.5 Z' fill='none'/>
                     <path d='M 1.5 0.67 C 1.5 0.67 2.24 3.32 2.24 5.47 C 2.24 7.53 0.89 9.2 -1.17 9.2 C -3.23 9.2 -4.79 7.53 -4.79 5.47 L -4.76 5.11 C -6.78 7.51 -8 10.62 -8 13.99 C -8 18.41 -4.42 22 0 22 C 4.42 22 8 18.41 8 13.99 C 8 8.6 5.41 3.79 1.5 0.67 Z M -0.29 19 C -2.07 19 -3.51 17.6 -3.51 15.86 C -3.51 14.24 -2.46 13.1 -0.7 12.74 C 1.07 12.38 2.9 11.53 3.92 10.16 C 4.31 11.45 4.51 12.81 4.51 14.2 C 4.51 16.85 2.36 19 -0.29 19 Z' fill='{$theme["fire"]}' stroke-opacity='0'/>
                 </g>
@@ -527,21 +560,21 @@ function generateCard(array $stats, array $params = null): string
             </g>
             <g style='isolation: isolate'>
                 <!-- Longest Streak big number -->
-                <g transform='translate({$longestStreakOffset},48)'>
+                <g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[0]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideNums"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.2s'>
                         {$longestStreak}
                     </text>
                 </g>
 
                 <!-- Longest Streak label -->
-                <g transform='translate({$longestStreakOffset},84)'>
+                <g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[1]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["sideLabels"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.3s'>
                         {$longestStreakText}
                     </text>
                 </g>
 
                 <!-- Longest Streak range -->
-                <g transform='translate({$longestStreakOffset},114)'>
+                <g transform='translate({$longestStreakOffset}, {$longestStreakHeightOffset[2]})'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='{$theme["dates"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.4s'>
                         {$longestStreakRange}
                     </text>
@@ -575,7 +608,13 @@ function generateErrorCard(string $message, array $params = null): string
     $rectWidth = $cardWidth - 1;
     $centerOffset = $cardWidth / 2;
 
-    return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation: isolate' viewBox='0 0 {$cardWidth} 195' width='{$cardWidth}px' height='195px'>
+    // read card_height parameter
+    $cardHeight = getCardHeight($params);
+    $rectHeight = $cardHeight - 1;
+    $heightOffset = ($cardHeight - 195) / 2;
+    $errorLabelOffset = $cardHeight / 2 + 10.5;
+
+    return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation: isolate' viewBox='0 0 {$cardWidth} {$cardHeight}' width='{$cardWidth}px' height='{$cardHeight}px'>
         <style>
             a {
                 fill: {$theme["dates"]};
@@ -583,17 +622,17 @@ function generateErrorCard(string $message, array $params = null): string
         </style>
         <defs>
             <clipPath id='outer_rectangle'>
-                <rect width='{$cardWidth}' height='195' rx='{$borderRadius}'/>
+                <rect width='{$cardWidth}' height='{$cardHeight}' rx='{$borderRadius}'/>
             </clipPath>
             {$theme["backgroundGradient"]}
         </defs>
         <g clip-path='url(#outer_rectangle)'>
             <g style='isolation: isolate'>
-                <rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='194'/>
+                <rect stroke='{$theme["border"]}' fill='{$theme["background"]}' rx='{$borderRadius}' x='0.5' y='0.5' width='{$rectWidth}' height='{$rectHeight}'/>
             </g>
             <g style='isolation: isolate'>
                 <!-- Error lable -->
-                <g transform='translate({$centerOffset},108)'>
+                <g transform='translate({$centerOffset}, {$errorLabelOffset})'>
                     <text x='0' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' fill='{$theme["sideLabels"]}' stroke='none' font-family='\"Segoe UI\", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal'>
                         {$message}
                     </text>
@@ -607,7 +646,7 @@ function generateErrorCard(string $message, array $params = null): string
                     </mask>
                 </defs>
                 <!-- Sad face -->
-                <g transform='translate({$centerOffset}, 0)'>
+                <g transform='translate({$centerOffset}, {$heightOffset})'>
                     <path fill='{$theme["fire"]}' d='M0,35.8c-25.2,0-45.7,20.5-45.7,45.7s20.5,45.8,45.7,45.8s45.7-20.5,45.7-45.7S25.2,35.8,0,35.8z M0,122.3c-11.2,0-21.4-4.5-28.8-11.9c-2.9-2.9-5.4-6.3-7.4-10c-3-5.7-4.6-12.1-4.6-18.9c0-22.5,18.3-40.8,40.8-40.8 c10.7,0,20.4,4.1,27.7,10.9c3.8,3.5,6.9,7.7,9.1,12.4c2.6,5.3,4,11.3,4,17.6C40.8,104.1,22.5,122.3,0,122.3z'/>
                     <path fill='{$theme["fire"]}' d='M4.8,93.8c5.4,1.1,10.3,4.2,13.7,8.6l3.9-3c-4.1-5.3-10-9-16.6-10.4c-10.6-2.2-21.7,1.9-28.3,10.4l3.9,3 C-13.1,95.3-3.9,91.9,4.8,93.8z'/>
                     <circle fill='{$theme["fire"]}' cx='-15' cy='71' r='4.9'/>
@@ -704,7 +743,7 @@ function convertHexColors(string $svg): string
  * @param int $cardWidth The width of the card
  * @return string The generated PNG data
  */
-function convertSvgToPng(string $svg, int $cardWidth): string
+function convertSvgToPng(string $svg, int $cardWidth, int $cardHeight): string
 {
     // trim off all whitespaces to make it a valid SVG string
     $svg = trim($svg);
@@ -722,7 +761,7 @@ function convertSvgToPng(string $svg, int $cardWidth): string
     // `--export-filename -`: write output to stdout
     // `-w 495 -h 195`: set width and height of the output image
     // `--export-type png`: set the output format to PNG
-    $cmd = "echo {$svg} | inkscape --pipe --export-filename - -w {$cardWidth} -h 195 --export-type png";
+    $cmd = "echo {$svg} | inkscape --pipe --export-filename - -w {$cardWidth} -h {$cardHeight} --export-type png";
 
     // convert svg to png
     $png = shell_exec($cmd); // skipcq: PHP-A1009
@@ -772,7 +811,8 @@ function generateOutput(string|array $output, array $params = null): array
         try {
             // extract width from SVG
             $cardWidth = (int) preg_replace("/.*width=[\"'](\d+)px[\"'].*/", "$1", $svg);
-            $png = convertSvgToPng($svg, $cardWidth);
+            $cardHeight = (int) preg_replace("/.*height=[\"'](\d+)px[\"'].*/", "$1", $svg);
+            $png = convertSvgToPng($svg, $cardWidth, $cardHeight);
             return [
                 "contentType" => "image/png",
                 "body" => $png,
diff --git a/src/demo/index.php b/src/demo/index.php
index 4da301c..3a74937 100644
--- a/src/demo/index.php
+++ b/src/demo/index.php
@@ -170,6 +170,9 @@ function camelToSkewer(string $str): string
                 <label for="card-width">Card Width</label>
                 <input class="param" type="number" id="card-width" name="card_width" placeholder="495" value="495" step="1" min="300" />
 
+                <label for="card-width">Card Height</label>
+                <input class="param" type="number" id="card-width" name="card_height" placeholder="195" value="195" step="1" min="170" />
+
                 <label for="type">Output Type</label>
                 <select class="param" id="type" name="type">
                     <option value="svg">SVG</option>
@@ -276,4 +279,4 @@ function camelToSkewer(string $str): string
     </a>
 </body>
 
-</html>
+</html>
\ No newline at end of file
diff --git a/src/demo/js/script.js b/src/demo/js/script.js
index 5e5b5e7..7ca85e5 100644
--- a/src/demo/js/script.js
+++ b/src/demo/js/script.js
@@ -15,6 +15,7 @@ const preview = {
     type: "svg",
     exclude_days: "",
     card_width: "495",
+    card_height: "195",
     hide_total_contributions: "false",
     hide_current_streak: "false",
     hide_longest_streak: "false",


Test patch:
diff --git a/tests/RenderTest.php b/tests/RenderTest.php
index 358b4af..0649213 100644
--- a/tests/RenderTest.php
+++ b/tests/RenderTest.php
@@ -233,6 +233,22 @@ final class RenderTest extends TestCase
         $this->assertStringContainsString("<g transform='translate(500,", $render);
     }
 
+    /**
+     * Test card height option
+     */
+    public function testCardHeight(): void
+    {
+        $this->testParams["card_height"] = "300";
+        $render = generateOutput($this->testStats, $this->testParams)["body"];
+        $this->assertStringContainsString("viewBox='0 0 495 300' width='495px' height='300px'", $render);
+        $this->assertStringContainsString("<rect width='495' height='300' rx='4.5'/>", $render);
+        $this->assertStringContainsString("<line x1='165' y1='54.25'", $render);
+        $this->assertStringContainsString("<line x1='330' y1='54.25'", $render);
+        $this->assertStringContainsString("<g transform='translate(82.5, 100.5)'>", $render);
+        $this->assertStringContainsString("<g transform='translate(247.5, 100.5)'>", $render);
+        $this->assertStringContainsString("<g transform='translate(412.5, 100.5)'>", $render);
+    }
+
     /**
      * Test first and third columns swapped when direction is rtl
      */
@@ -242,11 +258,11 @@ final class RenderTest extends TestCase
         $render = generateOutput($this->testStats, $this->testParams)["body"];
         $renderCollapsedSpaces = preg_replace("/(\s)\s*/", '$1', $render);
         $this->assertStringContainsString(
-            "<!-- Total Contributions big number -->\n<g transform='translate(412.5,48)'>",
+            "<!-- Total Contributions big number -->\n<g transform='translate(412.5, 48)'>",
             $renderCollapsedSpaces
         );
         $this->assertStringContainsString(
-            "<!-- Longest Streak big number -->\n<g transform='translate(82.5,48)'>",
+            "<!-- Longest Streak big number -->\n<g transform='translate(82.5, 48)'>",
             $renderCollapsedSpaces
         );
     }
diff --git a/tests/expected/test_card.svg b/tests/expected/test_card.svg
index ca488f0..39a74a6 100644
--- a/tests/expected/test_card.svg
+++ b/tests/expected/test_card.svg
@@ -31,21 +31,21 @@
             </g>
             <g style='isolation: isolate'>
                 <!-- Total Contributions big number -->
-                <g transform='translate(82.5,48)'>
+                <g transform='translate(82.5, 48)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#666666' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s'>
                         2,048
                     </text>
                 </g>
 
                 <!-- Total Contributions label -->
-                <g transform='translate(82.5,84)'>
+                <g transform='translate(82.5, 84)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#888888' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.7s'>
                         Total Contributions
                     </text>
                 </g>
 
                 <!-- Total Contributions range -->
-                <g transform='translate(82.5,114)'>
+                <g transform='translate(82.5, 114)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#999999' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.8s'>
                         Aug 10, 2016 - Present
                     </text>
@@ -53,21 +53,21 @@
             </g>
             <g style='isolation: isolate'>
                 <!-- Current Streak big number -->
-                <g transform='translate(247.5,48)'>
+                <g transform='translate(247.5, 48)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#555555' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='animation: currstreak 0.6s linear forwards'>
                         16
                     </text>
                 </g>
 
                 <!-- Current Streak label -->
-                <g transform='translate(247.5,108)'>
+                <g transform='translate(247.5, 108)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#777777' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
                         Current Streak
                     </text>
                 </g>
 
                 <!-- Current Streak range -->
-                <g transform='translate(247.5,145)'>
+                <g transform='translate(247.5, 145)'>
                     <text x='0' y='21' stroke-width='0' text-anchor='middle' fill='#999999' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 0.9s'>
                         Mar 28, 2019 - Apr 12, 2019
                     </text>
@@ -86,21 +86,21 @@
             </g>
             <g style='isolation: isolate'>
                 <!-- Longest Streak big number -->
-                <g transform='translate(412.5,48)'>
+                <g transform='translate(412.5, 48)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#666666' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='700' font-size='28px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.2s'>
                         86
                     </text>
                 </g>
 
                 <!-- Longest Streak label -->
-                <g transform='translate(412.5,84)'>
+                <g transform='translate(412.5, 84)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#888888' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.3s'>
                         Longest Streak
                     </text>
                 </g>
 
                 <!-- Longest Streak range -->
-                <g transform='translate(412.5,114)'>
+                <g transform='translate(412.5, 114)'>
                     <text x='0' y='32' stroke-width='0' text-anchor='middle' fill='#999999' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='12px' font-style='normal' style='opacity: 0; animation: fadein 0.5s linear forwards 1.4s'>
                         Dec 19, 2016 - Mar 14, 2016
                     </text>
diff --git a/tests/expected/test_error_card.svg b/tests/expected/test_error_card.svg
index 23c28d2..243faa5 100644
--- a/tests/expected/test_error_card.svg
+++ b/tests/expected/test_error_card.svg
@@ -16,7 +16,7 @@
             </g>
             <g style='isolation: isolate'>
                 <!-- Error lable -->
-                <g transform='translate(247.5,108)'>
+                <g transform='translate(247.5, 108)'>
                     <text x='0' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' fill='#888888' stroke='none' font-family='"Segoe UI", Ubuntu, sans-serif' font-weight='400' font-size='14px' font-style='normal'>
                         An unknown error occurred
                     </text>