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

Issue:
Add ability to set the output type to PNG
It may be useful to be able to directly get a PNG versions of the streak stats cards.

One way this can be done is by converting the SVG output to a PNG.

If necessary, ImageMagick or a similar library may be used to do this.

You may need the animations to be stripped out of the SVG before conversion.

You may use [this script](https://gist.github.com/DenverCoder1/955096c0d458e63016fc397c3e8d0edf) (written in Python) for inspiration. Since this project is written in PHP it would be preferable for the solution to this issue to be written in PHP.

There is currently a `type` option allowing output as `svg` or `json`, this can be added as a 3rd option.

Pull request description:
Add ability to output as PNG
## Description

This PR will add the ability to output the data as a PNG. In order to make this work you will need to have imagick installed and enabled.

There were a couple of changes that I had to make to the SVG as imagick had some issues regarding the `mask` attribute on the circle and the `dy` attributes on the text. 

In order to fix the mask I added an ellipse after adding the circle. This give the same end result. 

To fix the dy I had to eyeball the metrics generated from the created SVG and PNG. After that I adjusted the `y` attributes in order to align everything again.

Fixes #135

### Type of change

- [x] New feature (added a non-breaking change which adds functionality)
- [x] Updated documentation (updated the readme, templates, or other repo files)

## How Has This Been Tested?

- [x] Tested locally with a valid username
- [x] Tested locally with an invalid username
- [x] Ran tests with `composer test`

## Checklist:

- [x] 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
- [x] My changes generate no new warnings

## Screenshots

![image](https://user-images.githubusercontent.com/1311371/135716921-d91f39bc-dfd0-4de8-830f-4a58d6891dba.png)


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 7e29a17..5ee7ae1 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -11,6 +11,7 @@ Make sure your request is meaningful and you have tested the app locally before
 
 * [PHP 7.4+](https://www.apachefriends.org/index.html)
 * [Composer](https://getcomposer.org)
+* [Imagick](https://www.php.net/imagick)
 
 #### Linux
 
@@ -51,6 +52,13 @@ putenv("TOKEN=ghp_example123");
 putenv("USERNAME=DenverCoder1");
 ```
 
+### Install dependencies
+Run the following command to install all the required dependencies to work on this project.
+
+```bash
+composer install
+```
+
 ### Running the app locally
 
 ```bash
@@ -63,12 +71,6 @@ Open http://localhost:8000/demo/ to run the demo site
 
 ### Running the tests
 
-Before you can run tests, PHPUnit must be installed. You can install it using Composer by running the following command.
-
-```bash
-composer install
-```
-
 Run the following command to run the PHPUnit test script which will verify that the tested functionality is still working.
 
 ```bash
diff --git a/README.md b/README.md
index 798d77d..1246b6e 100644
--- a/README.md
+++ b/README.md
@@ -74,7 +74,7 @@ If the `theme` parameter is specified, any color customizations specified will b
 |   `sideLabels`    |        Total and longest streak labels         |       **hex code** without `#` or **css color**       |
 |      `dates`      |             Date range text color              |       **hex code** without `#` or **css color**       |
 |   `date_format`   |       Date format (Default: `M j[, Y]`)        |    See note below on [Date Formats](#date-formats)    |
-|      `type`       |         Output format (Default: `svg`)         |           Current options: `svg` or `json`            |
+|      `type`       |         Output format (Default: `svg`)         |       Current options: `svg`, `png` or `json`         |
 
 ### Date Formats
 
@@ -188,6 +188,7 @@ Make sure your request is meaningful and you have tested the app locally before
 
 - [PHP 7.4+](https://www.apachefriends.org/index.html)
 - [Composer](https://getcomposer.org)
+- [Imagick](https://www.php.net/imagick)
 
 #### Linux
 
@@ -212,6 +213,13 @@ git clone https://github.com/DenverCoder1/github-readme-streak-stats.git
 cd github-readme-streak-stats
 ```
 
+### Install dependencies
+Run the following command to install all the required dependencies to work on this project.
+
+```bash
+composer install
+```
+
 ### Authorization
 
 To get the GitHub API to run locally you will need to provide a token.
@@ -238,12 +246,6 @@ Open <http://localhost:8000/demo/> to run the demo site.
 
 ### Running the tests
 
-Before you can run tests, PHPUnit must be installed. You can install it using Composer by running the following command.
-
-```bash
-composer install
-```
-
 Run the following command to run the PHPUnit test script which will verify that the tested functionality is still working.
 
 ```bash
diff --git a/composer.json b/composer.json
index 0f55b8e..0cfd901 100644
--- a/composer.json
+++ b/composer.json
@@ -18,7 +18,8 @@
     ]
   },
   "require": {
-    "php": "^7.4|^8.0"
+    "php": "^7.4|^8.0",
+    "ext-imagick": "*"
   },
   "require-dev": {
     "phpunit/phpunit": "^9"
@@ -27,4 +28,4 @@
     "start": "php -S localhost:8000 -t src",
     "test": "./vendor/bin/phpunit --testdox tests"
   }
-}
\ No newline at end of file
+}
diff --git a/src/card.php b/src/card.php
index 00162aa..a7562b3 100644
--- a/src/card.php
+++ b/src/card.php
@@ -90,8 +90,8 @@ function generateCard(array $stats, array $params = null): string
     $theme = getRequestedTheme($params ?? $_REQUEST);
 
     // get date format
-    $dateFormat = isset(($params ?? $_REQUEST)["date_format"]) 
-        ? ($params ?? $_REQUEST)["date_format"] 
+    $dateFormat = isset(($params ?? $_REQUEST)["date_format"])
+        ? ($params ?? $_REQUEST)["date_format"]
         : "M j[, Y]";
 
     // total contributions
@@ -117,8 +117,7 @@ function generateCard(array $stats, array $params = null): string
         $longestStreakRange .= " - " . $longestStreakEnd;
     }
 
-    return "
-    <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
+    return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
         <style>
             @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700);
             @keyframes currstreak {
@@ -139,7 +138,7 @@ function generateCard(array $stats, array $params = null): string
         <g clip-path='url(#_clipPath_OZGVUqgkTHHpPTYeqOmK3uLgktRVSwWw)'>
             <g style='isolation:isolate'>
                 <path d='M 4.5 0 L 490.5 0 C 492.984 0 495 2.016 495 4.5 L 495 190.5 C 495 192.984 492.984 195 490.5 195 L 4.5 195 C 2.016 195 0 192.984 0 190.5 L 0 4.5 C 0 2.016 2.016 0 4.5 0 Z'
-                    style='stroke: {$theme["border"]}; fill: {$theme["background"]};stroke-miterlimit:10;rx: 4.5;'/>
+                      style='stroke: {$theme["border"]}; fill: {$theme["background"]};stroke-miterlimit:10;rx: 4.5;'/>
             </g>
             <g style='isolation:isolate'>
                 <line x1='330' y1='28' x2='330' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='{$theme["stroke"]}' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
@@ -149,7 +148,7 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Total Contributions Big Number -->
                 <g transform='translate(1,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:{$theme["sideNums"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:{$theme["sideNums"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
                         {$totalContributions}
                     </text>
                 </g>
@@ -157,7 +156,7 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Total Contributions Label -->
                 <g transform='translate(1,84)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
                         Total Contributions
                     </text>
                 </g>
@@ -165,7 +164,7 @@ function generateCard(array $stats, array $params = null): string
                 <!-- total contributions range -->
                 <g transform='translate(1,114)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:{$theme["dates"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.8s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:{$theme["dates"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.8s;'>
                         {$totalContributionsRange}
                     </text>
                 </g>
@@ -174,7 +173,7 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Current Streak Big Number -->
                 <g transform='translate(166,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:{$theme["currStreakNum"]};stroke:none;animation: currstreak 0.6s linear forwards;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:{$theme["currStreakNum"]};stroke:none;animation: currstreak 0.6s linear forwards;'>
                         {$currentStreak}
                     </text>
                 </g>
@@ -182,7 +181,7 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Current Streak Label -->
                 <g transform='translate(166,108)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:{$theme["currStreakLabel"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:{$theme["currStreakLabel"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
                         Current Streak
                     </text>
                 </g>
@@ -190,31 +189,26 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Current Streak Range -->
                 <g transform='translate(166,145)'>
                     <rect width='163' height='26' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='13' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:{$theme["dates"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
+                    <text x='81.5' y='21' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:{$theme["dates"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
                         {$currentStreakRange}
                     </text>
                 </g>
 
-                <!-- mask for background behind fire -->
-                <defs>
-                    <mask id='cut-off-area'>
-                    <rect x='0' y='0' width='500' height='500' fill='white' />
-                    <ellipse cx='247.5' cy='31' rx='13' ry='18'/>
-                    </mask>
-                </defs>
                 <!-- ring around number -->
-                <circle cx='247.5' cy='71' r='40' mask='url(#cut-off-area)' style='fill:none;stroke:{$theme["ring"]};stroke-width:5;opacity: 0; animation: fadein 0.5s linear forwards 0.4s;'></circle>
+                <circle cx='247.5' cy='71' r='40' style='fill:none;stroke:{$theme["ring"]};stroke-width:5;opacity: 0; animation: fadein 0.5s linear forwards 0.4s;'></circle>
+                <ellipse cx='247.5' cy='32' rx='13' ry='18' fill='{$theme["background"]}' />
                 <!-- fire icon -->
                 <g style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
                     <path d=' M 235.5 19.5 L 259.5 19.5 L 259.5 43.5 L 235.5 43.5 L 235.5 19.5 Z ' fill='none'/>
                     <path d=' M 249 20.17 C 249 20.17 249.74 22.82 249.74 24.97 C 249.74 27.03 248.39 28.7 246.33 28.7 C 244.26 28.7 242.7 27.03 242.7 24.97 L 242.73 24.61 C 240.71 27.01 239.5 30.12 239.5 33.5 C 239.5 37.92 243.08 41.5 247.5 41.5 C 251.92 41.5 255.5 37.92 255.5 33.5 C 255.5 28.11 252.91 23.3 249 20.17 Z  M 247.21 38.5 C 245.43 38.5 243.99 37.1 243.99 35.36 C 243.99 33.74 245.04 32.6 246.8 32.24 C 248.57 31.88 250.4 31.03 251.42 29.66 C 251.81 30.95 252.01 32.31 252.01 33.7 C 252.01 36.35 249.86 38.5 247.21 38.5 Z ' fill='{$theme["fire"]}'/>
                 </g>
+
             </g>
             <g style='isolation:isolate'>
                 <!-- Longest Streak Big Number -->
                 <g transform='translate(331,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:{$theme["sideNums"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 1.2s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:{$theme["sideNums"]};stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 1.2s;'>
                         {$longestStreak}
                     </text>
                 </g>
@@ -222,7 +216,7 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Longest Streak Label -->
                 <g transform='translate(331,84)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
                         Longest Streak
                     </text>
                 </g>
@@ -230,14 +224,14 @@ function generateCard(array $stats, array $params = null): string
                 <!-- Longest Streak Range -->
                 <g transform='translate(331,114)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:{$theme["dates"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.4s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:{$theme["dates"]};stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.4s;'>
                         {$longestStreakRange}
                     </text>
                 </g>
             </g>
         </g>
     </svg>
-    ";
+";
 }
 
 /**
@@ -253,8 +247,7 @@ function generateErrorCard(string $message, array $params = null): string
     // get requested theme, use $_REQUEST if no params array specified
     $theme = getRequestedTheme($params ?? $_REQUEST);
 
-    return "
-    <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
+    return "<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
         <style>
             @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700);
         </style>
@@ -272,7 +265,7 @@ function generateErrorCard(string $message, array $params = null): string
                 <!-- Error Label -->
                 <g transform='translate(166,108)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none;'>
+                    <text x='81.5' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:{$theme["sideLabels"]};stroke:none;'>
                         {$message}
                     </text>
                 </g>
@@ -294,5 +287,49 @@ function generateErrorCard(string $message, array $params = null): string
             </g>
         </g>
     </svg>
-    ";
+";
+}
+
+/**
+ * Displays a card as an SVG image
+ *
+ * @param string $svg The SVG for the card to display
+ */
+function echoAsSvg(string $svg): void {
+    // set content type to SVG image
+    header("Content-Type: image/svg+xml");
+
+    // echo SVG data for streak stats
+    echo $svg;
+}
+
+/**
+ * Displays a card as a PNG image
+ *
+ * @param string $svg The SVG for the card to display
+ *
+ * @throws ImagickException
+ */
+function echoAsPng(string $svg): void {
+    // remove style and animations
+    $svg = preg_replace('/(<style>\X*<\/style>)/m', '', $svg);
+    $svg = preg_replace('/(opacity: 0;)/m', 'opacity: 1;', $svg);
+    $svg = preg_replace('/(animation: fadein.*?;)/m', 'opacity: 1;', $svg);
+    $svg = preg_replace('/(animation: currentstreak.*?;)/m', 'font-size: 28px;', $svg);
+
+    // create canvas
+    $imagick = new Imagick();
+    $imagick->setBackgroundColor(new ImagickPixel('transparent'));
+
+    // add svg image
+    $imagick->readImageBlob($svg);
+    $imagick->setImageFormat('png');
+
+    // echo PNG data
+    header('Content-Type: image/png');
+    echo $imagick->getImageBlob();
+
+    // clean up memory
+    $imagick->clear();
+    $imagick->destroy();
 }
diff --git a/src/index.php b/src/index.php
index 0b78948..c8501cf 100644
--- a/src/index.php
+++ b/src/index.php
@@ -8,14 +8,26 @@ require_once "card.php";
 if (file_exists("config.php")) {
     require_once "config.php";
 }
+
+$requestedType = $_REQUEST['type'] ?? 'svg';
+
 // if environment variables are not loaded, display error
 if (!getenv("TOKEN") || !getenv("USERNAME")) {
     $message = file_exists("config.php")
     ? "Missing token or username in config. Check Contributing.md for details."
     : "src/config.php was not found. Check Contributing.md for details.";
-    die(generateErrorCard($message));
+
+
+    $card = generateErrorCard($message);
+    if ($requestedType === "png") {
+        echoAsPng($card);
+    }
+    echoAsSvg($card);
+
+    exit;
 }
 
+
 // set cache to refresh once per day
 $timestamp = gmdate("D, d M Y 23:59:00") . " GMT";
 header("Expires: $timestamp");
@@ -35,10 +47,16 @@ try {
     $contributions = getContributionDates($contributionGraphs);
     $stats = getContributionStats($contributions);
 } catch (InvalidArgumentException $error) {
-    die(generateErrorCard($error->getMessage()));
+    $card = generateErrorCard($error->getMessage());
+    if ($requestedType === "png") {
+        echoAsPng($card);
+    }
+    echoAsSvg($card);
+
+    exit;
 }
 
-if (isset($_REQUEST["type"]) && $_REQUEST["type"] === "json") {
+if ($requestedType === "json") {
     // set content type to JSON
     header('Content-Type: application/json');
     // echo JSON data for streak stats
@@ -47,8 +65,8 @@ if (isset($_REQUEST["type"]) && $_REQUEST["type"] === "json") {
     exit;
 }
 
-// set content type to SVG image
-header("Content-Type: image/svg+xml");
-
-// echo SVG data for streak stats
-echo generateCard($stats);
\ No newline at end of file
+$card = generateCard($stats);
+if ($requestedType === "png") {
+    echoAsPng($card);
+}
+echoAsSvg($card);


Test patch:
diff --git a/tests/expected/test_card.svg b/tests/expected/test_card.svg
index 828cd82..4473207 100644
--- a/tests/expected/test_card.svg
+++ b/tests/expected/test_card.svg
@@ -1,5 +1,4 @@
-
-    <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
         <style>
             @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700);
             @keyframes currstreak {
@@ -20,7 +19,7 @@
         <g clip-path='url(#_clipPath_OZGVUqgkTHHpPTYeqOmK3uLgktRVSwWw)'>
             <g style='isolation:isolate'>
                 <path d='M 4.5 0 L 490.5 0 C 492.984 0 495 2.016 495 4.5 L 495 190.5 C 495 192.984 492.984 195 490.5 195 L 4.5 195 C 2.016 195 0 192.984 0 190.5 L 0 4.5 C 0 2.016 2.016 0 4.5 0 Z'
-                    style='stroke: #111111; fill: #000000;stroke-miterlimit:10;rx: 4.5;'/>
+                      style='stroke: #111111; fill: #000000;stroke-miterlimit:10;rx: 4.5;'/>
             </g>
             <g style='isolation:isolate'>
                 <line x1='330' y1='28' x2='330' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='#222222' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
@@ -30,7 +29,7 @@
                 <!-- Total Contributions Big Number -->
                 <g transform='translate(1,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
                         2048
                     </text>
                 </g>
@@ -38,7 +37,7 @@
                 <!-- Total Contributions Label -->
                 <g transform='translate(1,84)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
                         Total Contributions
                     </text>
                 </g>
@@ -46,7 +45,7 @@
                 <!-- total contributions range -->
                 <g transform='translate(1,114)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.8s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.8s;'>
                         Aug 10, 2016 - Present
                     </text>
                 </g>
@@ -55,7 +54,7 @@
                 <!-- Current Streak Big Number -->
                 <g transform='translate(166,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#555555;stroke:none;animation: currstreak 0.6s linear forwards;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#555555;stroke:none;animation: currstreak 0.6s linear forwards;'>
                         16
                     </text>
                 </g>
@@ -63,7 +62,7 @@
                 <!-- Current Streak Label -->
                 <g transform='translate(166,108)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:#777777;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:#777777;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
                         Current Streak
                     </text>
                 </g>
@@ -71,31 +70,26 @@
                 <!-- Current Streak Range -->
                 <g transform='translate(166,145)'>
                     <rect width='163' height='26' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='13' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
+                    <text x='81.5' y='21' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
                         Mar 28, 2019 - Apr 12, 2019
                     </text>
                 </g>
 
-                <!-- mask for background behind fire -->
-                <defs>
-                    <mask id='cut-off-area'>
-                    <rect x='0' y='0' width='500' height='500' fill='white' />
-                    <ellipse cx='247.5' cy='31' rx='13' ry='18'/>
-                    </mask>
-                </defs>
                 <!-- ring around number -->
-                <circle cx='247.5' cy='71' r='40' mask='url(#cut-off-area)' style='fill:none;stroke:#333333;stroke-width:5;opacity: 0; animation: fadein 0.5s linear forwards 0.4s;'></circle>
+                <circle cx='247.5' cy='71' r='40' style='fill:none;stroke:#333333;stroke-width:5;opacity: 0; animation: fadein 0.5s linear forwards 0.4s;'></circle>
+                <ellipse cx='247.5' cy='32' rx='13' ry='18' fill='#000000' />
                 <!-- fire icon -->
                 <g style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
                     <path d=' M 235.5 19.5 L 259.5 19.5 L 259.5 43.5 L 235.5 43.5 L 235.5 19.5 Z ' fill='none'/>
                     <path d=' M 249 20.17 C 249 20.17 249.74 22.82 249.74 24.97 C 249.74 27.03 248.39 28.7 246.33 28.7 C 244.26 28.7 242.7 27.03 242.7 24.97 L 242.73 24.61 C 240.71 27.01 239.5 30.12 239.5 33.5 C 239.5 37.92 243.08 41.5 247.5 41.5 C 251.92 41.5 255.5 37.92 255.5 33.5 C 255.5 28.11 252.91 23.3 249 20.17 Z  M 247.21 38.5 C 245.43 38.5 243.99 37.1 243.99 35.36 C 243.99 33.74 245.04 32.6 246.8 32.24 C 248.57 31.88 250.4 31.03 251.42 29.66 C 251.81 30.95 252.01 32.31 252.01 33.7 C 252.01 36.35 249.86 38.5 247.21 38.5 Z ' fill='#444444'/>
                 </g>
+
             </g>
             <g style='isolation:isolate'>
                 <!-- Longest Streak Big Number -->
                 <g transform='translate(331,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 1.2s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 1.2s;'>
                         86
                     </text>
                 </g>
@@ -103,7 +97,7 @@
                 <!-- Longest Streak Label -->
                 <g transform='translate(331,84)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
                         Longest Streak
                     </text>
                 </g>
@@ -111,11 +105,10 @@
                 <!-- Longest Streak Range -->
                 <g transform='translate(331,114)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.4s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.4s;'>
                         Dec 19, 2016 - Mar 14, 2016
                     </text>
                 </g>
             </g>
         </g>
     </svg>
-    
\ No newline at end of file
diff --git a/tests/expected/test_date_card.svg b/tests/expected/test_date_card.svg
index ed310c6..466889e 100644
--- a/tests/expected/test_date_card.svg
+++ b/tests/expected/test_date_card.svg
@@ -1,5 +1,4 @@
-
-    <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
         <style>
             @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700);
             @keyframes currstreak {
@@ -20,7 +19,7 @@
         <g clip-path='url(#_clipPath_OZGVUqgkTHHpPTYeqOmK3uLgktRVSwWw)'>
             <g style='isolation:isolate'>
                 <path d='M 4.5 0 L 490.5 0 C 492.984 0 495 2.016 495 4.5 L 495 190.5 C 495 192.984 492.984 195 490.5 195 L 4.5 195 C 2.016 195 0 192.984 0 190.5 L 0 4.5 C 0 2.016 2.016 0 4.5 0 Z'
-                    style='stroke: #111111; fill: #000000;stroke-miterlimit:10;rx: 4.5;'/>
+                      style='stroke: #111111; fill: #000000;stroke-miterlimit:10;rx: 4.5;'/>
             </g>
             <g style='isolation:isolate'>
                 <line x1='330' y1='28' x2='330' y2='170' vector-effect='non-scaling-stroke' stroke-width='1' stroke='#222222' stroke-linejoin='miter' stroke-linecap='square' stroke-miterlimit='3'/>
@@ -30,7 +29,7 @@
                 <!-- Total Contributions Big Number -->
                 <g transform='translate(1,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
                         2048
                     </text>
                 </g>
@@ -38,7 +37,7 @@
                 <!-- Total Contributions Label -->
                 <g transform='translate(1,84)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.7s;'>
                         Total Contributions
                     </text>
                 </g>
@@ -46,7 +45,7 @@
                 <!-- total contributions range -->
                 <g transform='translate(1,114)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.8s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 0.8s;'>
                         2016-08-10 - Present
                     </text>
                 </g>
@@ -55,7 +54,7 @@
                 <!-- Current Streak Big Number -->
                 <g transform='translate(166,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#555555;stroke:none;animation: currstreak 0.6s linear forwards;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#555555;stroke:none;animation: currstreak 0.6s linear forwards;'>
                         16
                     </text>
                 </g>
@@ -63,7 +62,7 @@
                 <!-- Current Streak Label -->
                 <g transform='translate(166,108)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:#777777;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:14px;font-style:normal;fill:#777777;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
                         Current Streak
                     </text>
                 </g>
@@ -71,31 +70,26 @@
                 <!-- Current Streak Range -->
                 <g transform='translate(166,145)'>
                     <rect width='163' height='26' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='13' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
+                    <text x='81.5' y='21' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 0.9s;'>
                         2019-03-28 - 04-12
                     </text>
                 </g>
 
-                <!-- mask for background behind fire -->
-                <defs>
-                    <mask id='cut-off-area'>
-                    <rect x='0' y='0' width='500' height='500' fill='white' />
-                    <ellipse cx='247.5' cy='31' rx='13' ry='18'/>
-                    </mask>
-                </defs>
                 <!-- ring around number -->
-                <circle cx='247.5' cy='71' r='40' mask='url(#cut-off-area)' style='fill:none;stroke:#333333;stroke-width:5;opacity: 0; animation: fadein 0.5s linear forwards 0.4s;'></circle>
+                <circle cx='247.5' cy='71' r='40' style='fill:none;stroke:#333333;stroke-width:5;opacity: 0; animation: fadein 0.5s linear forwards 0.4s;'></circle>
+                <ellipse cx='247.5' cy='32' rx='13' ry='18' fill='#000000' />
                 <!-- fire icon -->
                 <g style='opacity: 0; animation: fadein 0.5s linear forwards 0.6s;'>
                     <path d=' M 235.5 19.5 L 259.5 19.5 L 259.5 43.5 L 235.5 43.5 L 235.5 19.5 Z ' fill='none'/>
                     <path d=' M 249 20.17 C 249 20.17 249.74 22.82 249.74 24.97 C 249.74 27.03 248.39 28.7 246.33 28.7 C 244.26 28.7 242.7 27.03 242.7 24.97 L 242.73 24.61 C 240.71 27.01 239.5 30.12 239.5 33.5 C 239.5 37.92 243.08 41.5 247.5 41.5 C 251.92 41.5 255.5 37.92 255.5 33.5 C 255.5 28.11 252.91 23.3 249 20.17 Z  M 247.21 38.5 C 245.43 38.5 243.99 37.1 243.99 35.36 C 243.99 33.74 245.04 32.6 246.8 32.24 C 248.57 31.88 250.4 31.03 251.42 29.66 C 251.81 30.95 252.01 32.31 252.01 33.7 C 252.01 36.35 249.86 38.5 247.21 38.5 Z ' fill='#444444'/>
                 </g>
+
             </g>
             <g style='isolation:isolate'>
                 <!-- Longest Streak Big Number -->
                 <g transform='translate(331,48)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 1.2s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:700;font-size:28px;font-style:normal;fill:#666666;stroke:none; opacity: 0; animation: fadein 0.5s linear forwards 1.2s;'>
                         86
                     </text>
                 </g>
@@ -103,7 +97,7 @@
                 <!-- Longest Streak Label -->
                 <g transform='translate(331,84)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.3s;'>
                         Longest Streak
                     </text>
                 </g>
@@ -111,11 +105,10 @@
                 <!-- Longest Streak Range -->
                 <g transform='translate(331,114)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='25' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.4s;'>
+                    <text x='81.5' y='32' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:12px;font-style:normal;fill:#999999;stroke:none;opacity: 0; animation: fadein 0.5s linear forwards 1.4s;'>
                         2016-12-19 - 2016-03-14
                     </text>
                 </g>
             </g>
         </g>
     </svg>
-    
\ No newline at end of file
diff --git a/tests/expected/test_error_card.svg b/tests/expected/test_error_card.svg
index 8a266ac..1666d91 100644
--- a/tests/expected/test_error_card.svg
+++ b/tests/expected/test_error_card.svg
@@ -1,5 +1,4 @@
-
-    <svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
+<svg xmlns='http://www.w3.org/2000/svg' xmlns:xlink='http://www.w3.org/1999/xlink' style='isolation:isolate' viewBox='0 0 495 195' width='495px' height='195px'>
         <style>
             @import url(https://fonts.googleapis.com/css?family=Open+Sans:400,700);
         </style>
@@ -17,7 +16,7 @@
                 <!-- Error Label -->
                 <g transform='translate(166,108)'>
                     <rect width='163' height='50' stroke='none' fill='none'></rect>
-                    <text x='81.5' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:&quot;Open Sans&quot;, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none;'>
+                    <text x='81.5' y='50' dy='0.25em' stroke-width='0' text-anchor='middle' style='font-family:Open Sans, Roboto, system-ui, sans-serif;font-weight:400;font-size:14px;font-style:normal;fill:#888888;stroke:none;'>
                         An unknown error occurred
                     </text>
                 </g>
@@ -39,4 +38,3 @@
             </g>
         </g>
     </svg>
-    
\ No newline at end of file