Merge pull request #18 from DenverCoder1/fetch-google-fonts

pull/20/head
Jonah Lawrence 3 years ago committed by GitHub
commit 1d1195e3a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

1
.gitignore vendored

@ -1,2 +1,3 @@
vendor/
.vscode/
.env

@ -64,7 +64,7 @@ Feel free to open a PR and add yours!
| `center` | `true` to center text or `false` for left aligned (default: `false`) | boolean | `true` or `false` |
| `height` | Height of the output SVG in pixels (default: `50`) | integer | Any positive number |
| `width` | Width of the output SVG in pixels (default: `400`) | integer | Any positive number |
| `font` | Font family (default: `JetBrains Mono`) | string | Any font from Google Fonts |
| `font` | Font family (default: `monospace`) | string | Any font from Google Fonts |
| `size` | Font size in pixels (default: `20`) | integer | Any positive number |
| `color` | Color of the text (default: `36BCF7`) | string | Hex code without # (eg. `00ff00`) |

@ -7,17 +7,19 @@
"autoload": {
"classmap": [
"src/models/",
"src/views/"
"src/views/",
"src/controllers/"
]
},
"require": {
"php": "^7.4|^8.0"
"php": "^7.4|^8.0",
"vlucas/phpdotenv": "^5.3"
},
"require-dev": {
"phpunit/phpunit": "^9"
},
"scripts": {
"start": "php -S localhost:8000 -t src",
"start": "php7 -S localhost:8000 -t src || php -S localhost:8000 -t src",
"test": "./vendor/bin/phpunit --testdox tests"
}
}

541
composer.lock generated

@ -4,8 +4,466 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "05ba91a252ac990d7436c4da90e71c46",
"packages": [],
"content-hash": "ef52ddfbec8f55a7fda17b32093476ac",
"packages": [
{
"name": "graham-campbell/result-type",
"version": "v1.0.1",
"source": {
"type": "git",
"url": "https://github.com/GrahamCampbell/Result-Type.git",
"reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/GrahamCampbell/Result-Type/zipball/7e279d2cd5d7fbb156ce46daada972355cea27bb",
"reference": "7e279d2cd5d7fbb156ce46daada972355cea27bb",
"shasum": ""
},
"require": {
"php": "^7.0|^8.0",
"phpoption/phpoption": "^1.7.3"
},
"require-dev": {
"phpunit/phpunit": "^6.5|^7.5|^8.5|^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0-dev"
}
},
"autoload": {
"psr-4": {
"GrahamCampbell\\ResultType\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Graham Campbell",
"email": "graham@alt-three.com"
}
],
"description": "An Implementation Of The Result Type",
"keywords": [
"Graham Campbell",
"GrahamCampbell",
"Result Type",
"Result-Type",
"result"
],
"support": {
"issues": "https://github.com/GrahamCampbell/Result-Type/issues",
"source": "https://github.com/GrahamCampbell/Result-Type/tree/v1.0.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/graham-campbell/result-type",
"type": "tidelift"
}
],
"time": "2020-04-13T13:17:36+00:00"
},
{
"name": "phpoption/phpoption",
"version": "1.7.5",
"source": {
"type": "git",
"url": "https://github.com/schmittjoh/php-option.git",
"reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/schmittjoh/php-option/zipball/994ecccd8f3283ecf5ac33254543eb0ac946d525",
"reference": "994ecccd8f3283ecf5ac33254543eb0ac946d525",
"shasum": ""
},
"require": {
"php": "^5.5.9 || ^7.0 || ^8.0"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"phpunit/phpunit": "^4.8.35 || ^5.7.27 || ^6.5.6 || ^7.0 || ^8.0 || ^9.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.7-dev"
}
},
"autoload": {
"psr-4": {
"PhpOption\\": "src/PhpOption/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Johannes M. Schmitt",
"email": "schmittjoh@gmail.com"
},
{
"name": "Graham Campbell",
"email": "graham@alt-three.com"
}
],
"description": "Option Type for PHP",
"keywords": [
"language",
"option",
"php",
"type"
],
"support": {
"issues": "https://github.com/schmittjoh/php-option/issues",
"source": "https://github.com/schmittjoh/php-option/tree/1.7.5"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/phpoption/phpoption",
"type": "tidelift"
}
],
"time": "2020-07-20T17:29:33+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1",
"reference": "5232de97ee3b75b0360528dae24e73db49566ab1",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-mbstring": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Mbstring\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for the Mbstring extension",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"mbstring",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-22T09:19:47+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91",
"reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"files": [
"bootstrap.php"
],
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "vlucas/phpdotenv",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/vlucas/phpdotenv.git",
"reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/vlucas/phpdotenv/zipball/b3eac5c7ac896e52deab4a99068e3f4ab12d9e56",
"reference": "b3eac5c7ac896e52deab4a99068e3f4ab12d9e56",
"shasum": ""
},
"require": {
"ext-pcre": "*",
"graham-campbell/result-type": "^1.0.1",
"php": "^7.1.3 || ^8.0",
"phpoption/phpoption": "^1.7.4",
"symfony/polyfill-ctype": "^1.17",
"symfony/polyfill-mbstring": "^1.17",
"symfony/polyfill-php80": "^1.17"
},
"require-dev": {
"bamarni/composer-bin-plugin": "^1.4.1",
"ext-filter": "*",
"phpunit/phpunit": "^7.5.20 || ^8.5.14 || ^9.5.1"
},
"suggest": {
"ext-filter": "Required to use the boolean validator."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "5.3-dev"
}
},
"autoload": {
"psr-4": {
"Dotenv\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Graham Campbell",
"email": "graham@alt-three.com",
"homepage": "https://gjcampbell.co.uk/"
},
{
"name": "Vance Lucas",
"email": "vance@vancelucas.com",
"homepage": "https://vancelucas.com/"
}
],
"description": "Loads environment variables from `.env` to `getenv()`, `$_ENV` and `$_SERVER` automagically.",
"keywords": [
"dotenv",
"env",
"environment"
],
"support": {
"issues": "https://github.com/vlucas/phpdotenv/issues",
"source": "https://github.com/vlucas/phpdotenv/tree/v5.3.0"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/vlucas/phpdotenv",
"type": "tidelift"
}
],
"time": "2021-01-20T15:23:13+00:00"
}
],
"packages-dev": [
{
"name": "doctrine/instantiator",
@ -1911,85 +2369,6 @@
],
"time": "2020-09-28T06:39:44+00:00"
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.22.1",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e",
"reference": "c6c942b1ac76c82448322025e084cadc56048b4e",
"shasum": ""
},
"require": {
"php": ">=7.1"
},
"suggest": {
"ext-ctype": "For best performance"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.22-dev"
},
"thanks": {
"name": "symfony/polyfill",
"url": "https://github.com/symfony/polyfill"
}
},
"autoload": {
"psr-4": {
"Symfony\\Polyfill\\Ctype\\": ""
},
"files": [
"bootstrap.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Gert de Pagter",
"email": "BackEndTea@gmail.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill for ctype functions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"ctype",
"polyfill",
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.22.1"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2021-01-07T16:49:33+00:00"
},
{
"name": "theseer/tokenizer",
"version": "1.2.0",

@ -0,0 +1,107 @@
<?php declare (strict_types = 1);
/**
* Controller for choosing model and rendering SVG outputs
*/
class RendererController
{
/**
* @var RendererModel $model
*/
private $model;
/**
* @var RendererView $view
*/
private $view;
/**
* @var array<string, string> $params
*/
private $params;
/**
* Construct RendererController
*
* @param array<string, string> $params request parameters
*/
public function __construct($params, $database = null)
{
$this->params = $params;
// create new database connection if none was passed
$database = $database ?? new DatabaseConnection();
// set up model and view
try {
// create renderer model
$this->model = new RendererModel(__DIR__ . "/../templates/main.php", $params, $database);
// create renderer view
$this->view = new RendererView($this->model);
} catch (Exception $error) {
// create error rendering model
$this->model = new ErrorModel(__DIR__ . "/../templates/error.php", $error->getMessage());
// create error rendering view
$this->view = new ErrorView($this->model);
}
}
/**
* Redirect to the demo site
*/
private function redirectToDemo(): void
{
header('Location: demo/');
exit;
}
/**
* Set content type for page output
*/
private function setContentType($type): void
{
header("Content-type: {$type}");
}
/**
* Set cache to refresh periodically
* This ensures any updates will roll out to all profiles
*/
private function setCacheRefreshDaily(): void
{
// set cache to refresh once per day
$timestamp = gmdate("D, d M Y 23:59:00") . " GMT";
header("Expires: $timestamp");
header("Last-Modified: $timestamp");
header("Pragma: no-cache");
header("Cache-Control: no-cache, must-revalidate");
}
/**
* Set output headers
*/
public function setHeaders(): void
{
// redirect to demo site if no text is given
if (!isset($this->params["lines"])) {
$this->redirectToDemo();
}
// set the content type header
$this->setContentType("image/svg+xml");
// set cache headers
$this->setCacheRefreshDaily();
}
/**
* Get the rendered SVG
*
* @return string The SVG to output
*/
public function render(): string
{
return $this->view->render();
}
}

@ -0,0 +1,78 @@
.loader,
.loader:before,
.loader:after {
display: none;
border-radius: 50%;
width: 2.5em;
height: 2.5em;
-webkit-animation-fill-mode: both;
animation-fill-mode: both;
-webkit-animation: load7 1.8s infinite ease-in-out;
animation: load7 1.8s infinite ease-in-out;
}
.loader {
color: var(--blue-light);
font-size: 10px;
margin: 0 0 38px 15px;
position: relative;
text-indent: -9999em;
-webkit-transform: translateY(-4px) translateZ(0) scale(0.5);
-ms-transform: translateY(-4px) translateZ(0) scale(0.5);
transform: translateY(-4px) translateZ(0) scale(0.5);
-webkit-animation-delay: -0.16s;
animation-delay: -0.16s;
}
.loader:before,
.loader:after {
content: '';
position: absolute;
top: 0;
}
.loader:before {
left: -3.5em;
-webkit-animation-delay: -0.32s;
animation-delay: -0.32s;
}
.loader:after {
left: 3.5em;
}
@-webkit-keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
@keyframes load7 {
0%,
80%,
100% {
box-shadow: 0 2.5em 0 -1.3em;
}
40% {
box-shadow: 0 2.5em 0 0;
}
}
.loading + .loader,
.loading + .loader:before,
.loading + .loader:after {
display: block;
}
.loading {
display: none;
}

@ -18,6 +18,7 @@
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Open+Sans&display=swap">
<link href="https://css.gg/css?=|moon|sun" rel="stylesheet">
<link rel="stylesheet" href="./css/style.css">
<link rel="stylesheet" href="./css/loader.css">
<link rel="stylesheet" href="./css/toggle-dark.css">
<script type="text/javascript" src="./js/script.js" defer></script>
<script type="text/javascript" src="./js/toggle-dark.js" defer></script>
@ -54,7 +55,7 @@
<h2>Options</h2>
<form class="parameters two-columns options">
<label for="font">Font</label>
<input class="param" type="text" id="font" name="font" placeholder="JetBrains Mono" value="JetBrains Mono"
<input class="param" type="text" id="font" name="font" placeholder="Open Sans" value="monospace"
pattern="^[A-Za-z0-9\- ]*$" title="Font from Google Fonts. Only letters, numbers, and spaces.">
<label for="color">Font color</label>
@ -81,7 +82,10 @@
<div class="output">
<h2>Preview</h2>
<img alt="Readme Typing SVG" src="/?lines=The+five+boxing+wizards+jump+quickly" />
<img alt="Readme Typing SVG" src="/?lines=The+five+boxing+wizards+jump+quickly"
onload="this.classList.remove('loading')" onerror="this.classList.remove('loading')" />
<div class="loader">Loading...</div>
<label class="show-border">
<input type="checkbox">

@ -1,7 +1,7 @@
let preview = {
// default values
defaults: {
font: "JetBrains Mono",
font: "",
color: "36BCF7",
size: "20",
center: "false",
@ -59,11 +59,13 @@ let preview = {
const md = `[![Typing SVG](${imageURL})](${repoLink})`;
// don't update if nothing has changed
const mdElement = document.querySelector(".md code");
const image = document.querySelector(".output img");
if (mdElement.innerText === md) {
return;
}
// update image preview
document.querySelector(".output img").src = demoImageURL;
image.src = demoImageURL;
image.classList.add("loading");
// update markdown
mdElement.innerText = md;
// disable copy button if no lines are filled in

@ -2,33 +2,10 @@
require '../vendor/autoload.php';
// set content type
header("Content-type: image/svg+xml");
// load environment variables if .env exists
$dotenv = Dotenv\Dotenv::createImmutable(__DIR__);
$dotenv->safeLoad();
// set cache to refresh once per day
$timestamp = gmdate("D, d M Y 23:59:00") . " GMT";
header("Expires: $timestamp");
header("Last-Modified: $timestamp");
header("Pragma: no-cache");
header("Cache-Control: no-cache, must-revalidate");
// redirect to demo site if no text is given
if (!isset($_REQUEST["lines"])) {
header('Location: demo/');
exit;
}
try {
// create renderer model
$model = new RendererModel("templates/main.php", $_REQUEST);
// create renderer view
$view = new RendererView($model);
} catch (InvalidArgumentException $error) {
// create error rendering model
$model = new ErrorModel("templates/error.php", $error->getMessage());
// create error rendering view
$view = new ErrorView($model);
}
// render SVG
echo $view->output();
$controller = new RendererController($_REQUEST);
$controller->setHeaders();
echo $controller->render();

@ -0,0 +1,79 @@
<?php
/**
* Class for accessing the font database
*/
class DatabaseConnection
{
/**
* PostgreSQL Database Connection
* @var resource|false
*/
private $conn = false;
/**
* Constructor for DatabaseConnection class
* Creates a database connection
*/
public function __construct()
{
// database is missing from env
if (!isset($_ENV["DATABASE_URL"])) {
return;
}
// connect to database
$conn_string = preg_replace(
"/^postgres:\/\/(.+?):(.+?)@(.+?):(\d+?)\/(.+)$/",
"host=$3 port=$4 dbname=$5 user=$1 password=$2",
$_ENV["DATABASE_URL"]
);
$this->conn = pg_connect($conn_string);
}
/**
* Fetch CSS from PostgreSQL Database
*
* @param string $font Google Font to fetch
* @return array<string> array containing the date and the CSS for displaying the font
*/
public function fetchFontCSS($font)
{
// check connection
if ($this->conn) {
// fetch font from database
$result = pg_query_params($this->conn, 'SELECT fetch_date, css FROM fonts WHERE family = $1', array($font));
if (!$result) {
return false;
}
$row = pg_fetch_row($result);
if ($row) {
return $row;
}
}
return false;
}
/**
* Insert font CSS into database
*
* @param string $font Font Family
* @param string $css CSS with Base64 encoding
* @return bool True if successful, false if connection failed
*/
public function insertFontCSS($font, $css)
{
if ($this->conn) {
$entry = array(
"family" => $font,
"css" => $css,
"fetch_date" => date('Y-m-d'),
);
$result = pg_insert($this->conn, 'fonts', $entry);
if (!$result) {
throw new InvalidArgumentException("Insertion of Google Font to database failed");
}
return true;
}
return false;
}
}

@ -5,17 +5,17 @@
*/
class ErrorModel
{
/** @var string $message text to display */
/** @var string $message Text to display */
public $message;
/** @var string $template path to template file */
/** @var string $template Path to template file */
public $template;
/**
* Construct ErrorModel
*
* @param string $message text to display
* @param string $template path to the template file
* @param string $message Text to display
* @param string $template Path to the template file
*/
public function __construct($template, $message)
{

@ -0,0 +1,71 @@
<?php
/**
* Class for converting Google Fonts to base 64 for displaying through SVG image
*/
class GoogleFontConverter
{
/**
* Fetch CSS from Google Fonts
*
* @param string $font Google Font to fetch
* @return string|false The CSS for displaying the font
*/
public static function fetchFontCSS($font)
{
$url = "https://fonts.googleapis.com/css2?family=" . str_replace(" ", "+", $font);
try {
// get the CSS for the font
$response = self::curl_get_contents($url);
// find all font files and convert them to base64 Data URIs
return self::encodeFonts($response);
} catch (InvalidArgumentException $error) {
return "";
}
}
/**
* Encode font urls in string as base 64
*
* @param string $css The CSS from Google Fonts
* @return string CSS with urls replaced with base 64 Data URIs
*/
private static function encodeFonts($css)
{
$urlRegex = '/\((https\:\/\/fonts\.gstatic\.com.+?)\) format\(\'(.*?)\'\)/';
preg_match_all($urlRegex, $css, $matches);
$urls = array_combine($matches[1], $matches[2]);
// go over all links and replace with data URI
foreach ($urls as $url => $fontType) {
$response = self::curl_get_contents($url);
$dataURI = "data:font/{$fontType};base64," . base64_encode($response);
$css = str_replace($url, $dataURI, $css);
}
return $css;
}
/**
* Get the contents of a URL
*
* @param string $url The URL to fetch
* @return string Response from URL
*/
private static function curl_get_contents($url): string
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_AUTOREFERER, true);
curl_setopt($ch, CURLOPT_HEADER, false);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true);
curl_setopt($ch, CURLOPT_VERBOSE, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($httpCode != 200) {
throw new InvalidArgumentException("Failed to fetch Google Font from API.");
}
return $response;
}
}

@ -8,16 +8,16 @@ class RendererModel
/** @var array<string> $lines text to display */
public $lines;
/** @var string $font font family */
/** @var string $font Font family */
public $font;
/** @var string $color font color */
/** @var string $color Font color */
public $color;
/** @var int $size font size */
/** @var int $size Font size */
public $size;
/** @var bool $center whether or not to center text */
/** @var bool $center Whether or not to center text */
public $center;
/** @var int $width SVG width (px) */
@ -26,12 +26,18 @@ class RendererModel
/** @var int $height SVG height (px) */
public $height;
/** @var string $template path to template file */
/** @var string $fontCSS CSS required for displaying the selected font */
public $fontCSS;
/** @var string $template Path to template file */
public $template;
/** @var DatabaseConnection $database Database connection */
private $database;
/** @var array<string, string> $DEFAULTS */
private $DEFAULTS = array(
"font" => "JetBrains Mono",
"font" => "monospace",
"color" => "#36BCF7",
"size" => "20",
"center" => "false",
@ -42,11 +48,14 @@ class RendererModel
/**
* Construct RendererModel
*
* @param string $template path to the template file
* @param string $template Path to the template file
* @param array<string, string> $params request parameters
* @param DatabaseConnection $font_db Database connection
*/
public function __construct($template, $params)
public function __construct($template, $params, $database)
{
$this->template = $template;
$this->database = $database;
$this->lines = $this->checkLines($params["lines"] ?? "");
$this->font = $this->checkFont($params["font"] ?? $this->DEFAULTS["font"]);
$this->color = $this->checkColor($params["color"] ?? $this->DEFAULTS["color"]);
@ -54,14 +63,14 @@ class RendererModel
$this->center = $this->checkCenter($params["center"] ?? $this->DEFAULTS["center"]);
$this->width = $this->checkNumber($params["width"] ?? $this->DEFAULTS["width"], "Width");
$this->height = $this->checkNumber($params["height"] ?? $this->DEFAULTS["height"], "Height");
$this->template = $template;
$this->fontCSS = $this->fetchFontCSS($this->font);
}
/**
* Validate lines and return array of string
*
* @param string $lines - semicolon separated lines parameter
* @return array<string>
* @param string $lines Semicolon-separated lines parameter
* @return array<string> escaped array of lines
*/
private function checkLines($lines)
{
@ -69,44 +78,45 @@ class RendererModel
throw new InvalidArgumentException("Lines parameter must be set.");
}
$exploded = explode(";", $lines);
// escape special characters to prevent code injection
return array_map("htmlspecialchars", $exploded);
}
/**
* Validate font family and return valid string
*
* @param string $font - font name parameter
* @return string
* @param string $font Font name parameter
* @return string Sanitized font name
*/
private function checkFont($font)
{
// return escaped font name
// return sanitized font name
return preg_replace("/[^0-9A-Za-z\- ]/", "", $font);
}
/**
* Validate font color and return valid string
*
* @param string $color - color parameter
* @return string
* @param string $color Color parameter
* @return string Sanitized color with preceding hash symbol
*/
private function checkColor($color)
{
$escaped = (string) preg_replace("/[^0-9A-Fa-f]/", "", $color);
$sanitized = (string) preg_replace("/[^0-9A-Fa-f]/", "", $color);
// if color is not a valid length, use the default
if (!in_array(strlen($escaped), [3, 4, 6, 8])) {
if (!in_array(strlen($sanitized), [3, 4, 6, 8])) {
return $this->DEFAULTS["color"];
}
// return escaped color
return "#" . $escaped;
// return sanitized color
return "#" . $sanitized;
}
/**
* Validate numeric parameter and return valid integer
*
* @param string $num - parameter to validate
* @param string $field - field name for displaying in case of error
* @return int
* @param string $num Parameter to validate
* @param string $field Field name for displaying in case of error
* @return int Sanitized digits and int
*/
private function checkNumber($num, $field)
{
@ -120,11 +130,43 @@ class RendererModel
/**
* Validate center alignment and return boolean
*
* @param string $center - center parameter
* @return boolean
* @param string $center Center parameter
* @return boolean Whether or not $center is set to "true"
*/
private function checkCenter($center)
{
return isset($center) ? ($center == "true") : $this->DEFAULTS["center"];
}
/**
* Fetch CSS with Base-64 encoding from database or store new entry if it is missing
*
* @param string $font Google Font to fetch
* @return string The CSS for displaying the font
*/
private function fetchFontCSS($font)
{
// skip checking if left as default
if ($font != $this->DEFAULTS["font"]) {
// fetch from database
$from_database = $this->database->fetchFontCSS($font);
if ($from_database) {
// return the CSS for displaying the font
$date = $from_database[0];
$css = $from_database[1];
return "<style>\n/* From database {$date} */\n{$css}</style>\n";
}
// fetch and convert from Google Fonts if not found in database
$from_google_fonts = GoogleFontConverter::fetchFontCSS($font);
if ($from_google_fonts) {
// add font to the database
$this->database->insertFontCSS($font, $from_google_fonts);
// return the CSS for displaying the font
$date = date('Y-m-d');
return "<style>\n/* From Google Fonts {$date} */\n{$from_google_fonts}</style>\n";
}
}
// font is not in database or Google Fonts
return "";
}
}

@ -4,11 +4,7 @@
viewBox='0 0 400 50'
width='400px' height='50px'>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono');
</style>
<text font-family='"JetBrains Mono", monospace' fill='#c00' font-size='18'
<text font-family='monospace' fill='#c00' font-size='18'
y="50%" x='50%' dominant-baseline='middle' text-anchor='middle'>
<?php echo $message . "\n" ?>
</text>

Before

Width:  |  Height:  |  Size: 500 B

After

Width:  |  Height:  |  Size: 388 B

@ -4,11 +4,9 @@
viewBox='0 0 <?php echo "$width $height" ?>'
width='<?php echo $width ?>px' height='<?php echo $height ?>px'>
<style>
@import url('https://fonts.googleapis.com/css2?family=<?php echo str_replace(" ", "+", $font) ?>')
</style>
<?php echo preg_replace("/\n/", "\n\t", $fontCSS); ?>
<?php $previousId = "d" . count($lines) - 1;?>
<?php $previousId = "d" . (count($lines) - 1);?>
<?php for ($i = 0; $i < count($lines); ++$i): ?>
<path id='path<?php echo $i ?>'>
<animate id='d<?php echo $i ?>' attributeName='d' begin='<?php echo ($i == 0 ? "0s;" : "") . $previousId ?>.end' dur='5s'

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

@ -23,13 +23,13 @@ class ErrorView
* Render SVG Output
* @return string
*/
public function output()
public function render()
{
// import variables into symbol table
extract(["message" => $this->model->message]);
// render SVG with output buffering
ob_start();
require_once $this->model->template;
include $this->model->template;
$output = ob_get_contents();
ob_end_clean();
// return rendered output

@ -23,7 +23,7 @@ class RendererView
* Render SVG Output
* @return string
*/
public function output()
public function render()
{
// import variables into symbol table
extract(array(
@ -34,10 +34,11 @@ class RendererView
"center" => $this->model->center,
"width" => $this->model->width,
"height" => $this->model->height,
"fontCSS" => $this->model->fontCSS,
));
// render SVG with output buffering
ob_start();
require_once $this->model->template;
include $this->model->template;
$output = ob_get_contents();
ob_end_clean();
// return rendered output

@ -5,6 +5,13 @@ require 'vendor/autoload.php';
final class OptionsTest extends TestCase
{
protected static $database;
public static function setUpBeforeClass(): void
{
self::$database = new DatabaseConnection();
}
/**
* Test exception thrown when missing 'lines' parameter
*/
@ -17,7 +24,7 @@ final class OptionsTest extends TestCase
"width" => "380",
"height" => "50",
);
print_r(new RendererModel("src/templates/main.php", $params));
print_r(new RendererModel("src/templates/main.php", $params, self::$database));
}
/**
@ -29,7 +36,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"font" => "Open Sans",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals("Open Sans", $model->font);
}
@ -42,7 +49,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"color" => "000000",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals("#000000", $model->color);
}
@ -55,7 +62,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"color" => "00000",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals("#36BCF7", $model->color);
}
@ -68,7 +75,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"size" => "18",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals(18, $model->size);
}
@ -83,7 +90,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"size" => "0",
);
print_r(new RendererModel("src/templates/main.php", $params));
print_r(new RendererModel("src/templates/main.php", $params, self::$database));
}
/**
@ -95,7 +102,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"height" => "80",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals(80, $model->height);
}
@ -110,7 +117,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"height" => "x",
);
print_r(new RendererModel("src/templates/main.php", $params));
print_r(new RendererModel("src/templates/main.php", $params, self::$database));
}
/**
@ -122,7 +129,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"width" => "500",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals(500, $model->width);
}
@ -137,7 +144,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"width" => "-1",
);
print_r(new RendererModel("src/templates/main.php", $params));
print_r(new RendererModel("src/templates/main.php", $params, self::$database));
}
/**
@ -149,7 +156,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"center" => "true",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals(true, $model->center);
}
@ -162,7 +169,7 @@ final class OptionsTest extends TestCase
"lines" => "text",
"center" => "other",
);
$model = new RendererModel("src/templates/main.php", $params);
$model = new RendererModel("src/templates/main.php", $params, self::$database);
$this->assertEquals(false, $model->center);
}
}

@ -5,6 +5,14 @@ require 'vendor/autoload.php';
final class RendererTest extends TestCase
{
protected static $database;
public static function setUpBeforeClass(): void
{
self::$database = new DatabaseConnection();
}
/**
* Test normal card render
*/
@ -21,9 +29,8 @@ final class RendererTest extends TestCase
"width" => "380",
"height" => "50",
);
$model = new RendererModel("src/templates/main.php", $params);
$view = new RendererView($model);
$this->assertEquals(file_get_contents("tests/svg/test_normal.svg"), $view->output());
$controller = new RendererController($params, self::$database);
$this->assertEquals(file_get_contents("tests/svg/test_normal.svg"), $controller->render());
}
/**
@ -37,15 +44,43 @@ final class RendererTest extends TestCase
"width" => "380",
"height" => "50",
);
try {
// create renderer model
$model = new RendererModel("src/templates/main.php", $params);
$view = new RendererView($model);
} catch (InvalidArgumentException $error) {
// create error rendering model
$model = new ErrorModel("src/templates/error.php", $error->getMessage());
$view = new ErrorView($model);
}
$this->assertEquals(file_get_contents("tests/svg/test_missing_lines.svg"), $view->output());
$controller = new RendererController($params, self::$database);
$this->assertEquals(file_get_contents("tests/svg/test_missing_lines.svg"), $controller->render());
}
/**
* Test loading a valid Google Font
*/
public function testLoadingGoogleFont(): void
{
$params = array(
"lines" => "text",
"font" => "Roboto",
);
$controller = new RendererController($params, self::$database);
$expected = preg_replace("/\/\*(.*?)\*\//", "", file_get_contents("tests/svg/test_fonts.svg"));
$actual = preg_replace("/\/\*(.*?)\*\//", "", $controller->render());
$this->assertEquals($expected, $actual);
}
/**
* Test loading a valid Google Font
*/
public function testInvalidGoogleFont(): void
{
$params = array(
"lines" => implode(";", array(
"Full-stack web and app developer",
"Self-taught UI/UX Designer",
"10+ years of coding experience",
"Always learning new things",
)),
"center" => "true",
"width" => "380",
"font" => "Not-A-Font",
);
$controller = new RendererController($params, self::$database);
$expected = str_replace('"monospace"', '"Not-A-Font"', file_get_contents("tests/svg/test_normal.svg"));
$this->assertEquals($expected, $controller->render());
}
}

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 170 KiB

@ -4,11 +4,7 @@
viewBox='0 0 400 50'
width='400px' height='50px'>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono');
</style>
<text font-family='"JetBrains Mono", monospace' fill='#c00' font-size='18'
<text font-family='monospace' fill='#c00' font-size='18'
y="50%" x='50%' dominant-baseline='middle' text-anchor='middle'>
Lines parameter must be set.
</text>

Before

Width:  |  Height:  |  Size: 499 B

After

Width:  |  Height:  |  Size: 387 B

@ -4,15 +4,12 @@
viewBox='0 0 380 50'
width='380px' height='50px'>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono')
</style>
<path id='path0'>
<animate id='d0' attributeName='d' begin='0s;d3.end' dur='5s'
values='m0,25 h0 ; m0,25 h380 ; m0,25 h0' keyTimes='0 ; 0.8 ; 1' />
</path>
<text font-family='"JetBrains Mono", monospace' fill='#36BCF7' font-size='20'
<text font-family='"monospace", monospace' fill='#36BCF7' font-size='20'
x='50%' dominant-baseline='middle' text-anchor='middle'>
<textPath xlink:href='#path0'>
Full-stack web and app developer
@ -23,7 +20,7 @@
<animate id='d1' attributeName='d' begin='d0.end' dur='5s'
values='m0,25 h0 ; m0,25 h380 ; m0,25 h0' keyTimes='0 ; 0.8 ; 1' />
</path>
<text font-family='"JetBrains Mono", monospace' fill='#36BCF7' font-size='20'
<text font-family='"monospace", monospace' fill='#36BCF7' font-size='20'
x='50%' dominant-baseline='middle' text-anchor='middle'>
<textPath xlink:href='#path1'>
Self-taught UI/UX Designer
@ -34,7 +31,7 @@
<animate id='d2' attributeName='d' begin='d1.end' dur='5s'
values='m0,25 h0 ; m0,25 h380 ; m0,25 h0' keyTimes='0 ; 0.8 ; 1' />
</path>
<text font-family='"JetBrains Mono", monospace' fill='#36BCF7' font-size='20'
<text font-family='"monospace", monospace' fill='#36BCF7' font-size='20'
x='50%' dominant-baseline='middle' text-anchor='middle'>
<textPath xlink:href='#path2'>
10+ years of coding experience
@ -45,7 +42,7 @@
<animate id='d3' attributeName='d' begin='d2.end' dur='5s'
values='m0,25 h0 ; m0,25 h380 ; m0,25 h0' keyTimes='0 ; 0.8 ; 1' />
</path>
<text font-family='"JetBrains Mono", monospace' fill='#36BCF7' font-size='20'
<text font-family='"monospace", monospace' fill='#36BCF7' font-size='20'
x='50%' dominant-baseline='middle' text-anchor='middle'>
<textPath xlink:href='#path3'>
Always learning new things

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Loading…
Cancel
Save