Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to output as PNG #140

Merged
merged 6 commits into from
Oct 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 8 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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
Expand Down
5 changes: 3 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
]
},
"require": {
"php": "^7.4|^8.0"
"php": "^7.4|^8.0",
"ext-imagick": "*"
},
"require-dev": {
"phpunit/phpunit": "^9"
Expand All @@ -27,4 +28,4 @@
"start": "php -S localhost:8000 -t src",
"test": "./vendor/bin/phpunit --testdox tests"
}
}
}
91 changes: 64 additions & 27 deletions src/card.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand All @@ -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'/>
Expand All @@ -149,23 +148,23 @@ 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>

<!-- 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>

<!-- 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>
Expand All @@ -174,70 +173,65 @@ 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>

<!-- 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>

<!-- 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>

<!-- 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>

<!-- 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>
";
";
}

/**
Expand All @@ -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>
Expand All @@ -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>
Expand All @@ -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();
}
34 changes: 26 additions & 8 deletions src/index.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,26 @@
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");
Expand All @@ -35,10 +47,16 @@
$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
Expand All @@ -47,8 +65,8 @@
exit;
}

// set content type to SVG image
header("Content-Type: image/svg+xml");

// echo SVG data for streak stats
echo generateCard($stats);
$card = generateCard($stats);
if ($requestedType === "png") {
echoAsPng($card);
}
echoAsSvg($card);