2

Short Question

I want to pass GDFONTPATH environment variable from Apache vhost.conf to PHP. How can I do this?

I am running: Debian 12, Apache 2.4, PHP 8.2

Long question

I am using GD image functions (e.g. imagettftext) in PHP and want to use specific fonts that may not be available system wide. This can be done by setting the environment variable GDFONTPATH and the PHP function putenv is good for this purpose.

However, putenv is also a security concern, and as such on my production servers, I have it listed on disable_functions in my php.ini and as a result, I can't set this environment var within my PHP script. Instead, setting it using SetEnv in my Apache vhost .conf file seems like a good idea (this is a cut-down example):

<Directory "/srv/www/example.com/public">
  AllowOverride None
  Require all granted
  SetEnv GDFONTPATH "/srv/www/example.com/private/fonts"
</Directory>

The snag is, this doesn't work as [I] expected! It does set GDFONTPATH, but not locally, so I get the following results:

getenv('GDFONTPATH');        # Returns '/srv/www/example.com/private/fonts'
getenv('GDFONTPATH', true);  # Returns ''

The libgd functions ignore this; they only look at the local environment variable that I only seem to be able to set using the putenv function. But enabling putenv on my production servers is not practical for security reasons, hence my really wanting to use SetEnv in my Apache config!

FWIW: I've coded a "pragmatic" work-around that will use PHP's getenv to read the path from Apache's SetEnv, and then pass a fully-qualified font-filename to gdlib. This works, but is less than ideal! I'll likely post this code for future reference, but I really would like a solution where I can set the environment from Apache. This post might offer a potential solution, but it looks like it would apply to all vhosts, so again, not ideal.

I'm a bit sketchy on the difference between environment variables and "local" environment variables.

References: libgd source regarding GDFONTPATH

Peter Ryan
  • 214
  • 1
  • 9
  • Don't you have a config file for your app where to put the font dir? – Olivier Aug 01 '23 at 12:04
  • Not really... I'm not using a framework of any description. But in any case, said framework would need to do "something" to set GDFONTPATH itself (or workaround like I'm doing now). – Peter Ryan Aug 01 '23 at 12:47
  • 2
    Passing the absolute path when calling `imagettftext()` is not a workaround. It's just one of the two available methods. And it's the one I would choose. – Olivier Aug 01 '23 at 12:51
  • Perhaps workaround is the wrong term. But it's not ideal as you then *must* specify the file extension and it also prevents the font being a "font-stack". I can probably live with that, but, I would still much prefer if I could set GDFONTPATH in the Apache config. – Peter Ryan Aug 01 '23 at 13:25
  • PeterRyan, I'm actually with @Olivier, not only is it not a workaround, if you do not call this behaviour ideal, I'd question if the system administration is ideal or not. I've placed an [answer that settled over the day](https://stackoverflow.com/a/76815908/367456), just comparing the name of an environment parameter is useless when the namespaces are different. It's a search path, when you know the absolute name, you don't need to search. Especially as you're not making use of system properties here for convenience, otherwise you'd already configured that parameter on the system. – hakre Aug 02 '23 at 00:24
  • Thank you both for your comments. I've accepted @hakre's answer below. – Peter Ryan Aug 02 '23 at 15:21

2 Answers2

1

SetEnv GDFONTPATH "..."

As you've already found out, SetEnv GDFONTPATH "..." sets Apache HTTPD internal variables, accessible through PHPs' SAPI via getenv() but not getenv(..., true) for system environment variables. Simply spoken, PHP getenv()s' $local = true variables aren't Apache internal environment variables.

GDFONTPATH

The GDFONTPATH system environment variable is in use by gdlib as the font-search-path parameter, that is similar to PATH, but for font files, not executable files.

This makes sense, e.g. to get access to fonts configured with the system, this is similar as to execute binaries on that system: Specifying the basename only suffices.

However, when your intend is to use a font file that is not configured on that system, again similar as with binaries, you need to provide the absolute pathname of the font that resolves to the directory entry of type file for that font, readable by the user the PHP process is running under. [credentials(7)]

$font_dirname = '/srv/www/example.com/private/fonts';
$font_basename = 'example.ttf';

$font_filename = $font_dirname . '/' . $font_basename;

set_error_handler(fn (int $type, string $message, string $file = null, int $line = null)
    => throw new ErrorException($message, 0, $type, $file, $line),
    E_ALL,
);

imagettftext($image, $size, $angle, $x, $y, $color, $font_filename, $text);

restore_error_handler();

Tested against:

  • PHP 8.2
  • GD library 2.3.3
  • GDFONTPATH unset

GDFONTPATH References:

hakre
  • 193,403
  • 52
  • 435
  • 836
  • 1
    Thanks for your reply (and also @Olivier's comments!). I've accepted this answer as I think it does best solve my problem, and it does make sense when I know the absolute path, I might as well just use that! My brain was fixed on the idea of overriding the GDFONTPATH path-list, but you're both absolutely right that it isn't really needed for this usage. Thanks again!! :D – Peter Ryan Aug 02 '23 at 15:17
  • 1
    Just for completeness (since I've dug through the libgd source code!), the "font" argument does allow for a semi-colon delimited list of fonts with or without file extension. The PHP docs state that ".ttf" is appended, but the source code actually tries .ttf, .pfa, .pfb, and .dfont in that order, which is *sometimes* useful when you want to treat it as a font stack. – Peter Ryan Aug 02 '23 at 15:20
0

If there is no way to pass GDFONTPATH via Apache config, then this is the workaround solution I'm using.

NOTE: This is based on the example code here.

<?php
// Functions very loosely based on Pythons os.path
// https://docs.python.org/3/library/os.path.html
function pathJoin(string $path, string ...$paths) {
    foreach ($paths as $segment) {
        if ($segment[0] === DIRECTORY_SEPARATOR) {
            $path = $segment;
        }
        else {
            if ($path[-1] !== DIRECTORY_SEPARATOR) {
                $path .= DIRECTORY_SEPARATOR;
            }
            $path .= $segment;
        }
    }
    return $path;
}

function pathSplit(string $path) {
    $pos = strrpos($path, DIRECTORY_SEPARATOR);
    if ($pos === false) {
        return ['', $path];
    }
    return [substr($path, 0, $pos + 1), substr($path, $pos + 1)];
}

function pathBasename(string $path) {
    return pathSplit($path)[1];
}


// Set the content-type
header('Content-Type: image/png');

// Create the image
$im = imagecreatetruecolor(400, 30);

// Create some colors
$white = imagecolorallocate($im, 255, 255, 255);
$grey = imagecolorallocate($im, 128, 128, 128);
$black = imagecolorallocate($im, 0, 0, 0);
imagefilledrectangle($im, 0, 0, 399, 29, $white);

// The text to draw
$text = 'Testing...';
$font = 'arial;msttcorefonts/Arial;dejavu/DejaVuSans';
// ^ this is a font-stack! So list of fonts to _try_

// Use Apache SetEnv directive to set GDFONTPATH. Since libgd will
// only use the "local" GDFONTPATH, this won't work directly, and
// the following code is required to build an absolute file-path:
if (getenv('GDFONTPATH', true) === false) {
    $gdFontPath = getenv('GDFONTPATH');
    if ($gdFontPath) {
        $delim = ';';
        $tok = strtok($font, $delim);
        while ($tok !== false) {
            $fontFile = pathJoin($gdFontPath, $tok);
            if (strrpos(pathBasename($fontFile), '.') === false) {
                $fontFile .= '.ttf';
            }
            if (file_exists($fontFile)) {
                $font = $fontFile;
                break;
            }
            $tok = strtok($delim);
        }
    }
}


// Add some shadow to the text
imagettftext($im, 20, 0, 11, 21, $grey, $font, $text);

// Add the text
imagettftext($im, 20, 0, 10, 20, $black, $font, $text);

// Using imagepng() results in clearer text compared with imagejpeg()
imagepng($im);
imagedestroy($im);

Differences from the php.net example code:

  1. I've added a few file path helper functions at the start.
  2. I'm using a "font-stack" rather than a single font like the original. The original had $font = 'arial.ttf'; but that didn't work on Debian and I suspect only really works on Windows. However, if the example used a font-stack, it could've listed multiple fonts/relative-paths, and that's what I'm doing in my example.
  3. I've added a chunk of code to check the GDFONTPATH environment variable (as supplied via Apache's SetEnv) and then use that to test each directory against each font listed in the stack. If/when it finds a match, it'll replace $font with an absolute file-path. Otherwise, it'll leave $font as it was.

This pretty much does what I want, but if SetEnv worked as I expected, this additional code wouldn't be required!

NOTE: I only test for the .ttf file extension whereas the libgd code tries .ttf, .pfa, .pfb, and .dfont extensions.

Peter Ryan
  • 214
  • 1
  • 9
  • 1
    _"If there is no way to pass GDFONTPATH via Apache config"_: there is, but you will certainly reject it as you would reject the system configuration (if you do, and the reasons are merely the same then). AFAIK it is not documented by/in the apache httpd project and certainly depends on packaging for a specific type of system (e.g. debian based): /etc/apache2/envvars . This is apache2 environment on such a system. [[apache2(8)](https://manpages.debian.org/bookworm/apache2-bin/apache2.8.en.html)], [[apache2ctl(8)](https://manpages.debian.org/bookworm/apache2/apache2ctl.8.en.html)] – hakre Aug 02 '23 at 09:17
  • 1
    Without wishing to belabour the point, the reason I wanted to set GDFONTPATH via Apache config was specifically so that I could keep it within it's virtual-host config file since it would've only been applicable to this specific vhost. As already established, this wasn't the best approach even if I could do it, but it made sense (to me) to set an absolute path to the vhost-specific fonts directory along with a bunch of other absolute paths related to the vhost in the one place. – Peter Ryan Aug 02 '23 at 18:13