Skip to content

Commit

Permalink
Merge 0832e1b into 53410a0
Browse files Browse the repository at this point in the history
  • Loading branch information
samwilson committed Dec 18, 2018
2 parents 53410a0 + 0832e1b commit baabcb0
Show file tree
Hide file tree
Showing 13 changed files with 141 additions and 27 deletions.
4 changes: 4 additions & 0 deletions .gitignore
Expand Up @@ -22,3 +22,7 @@
/.phpcs-cache
/phpcs.xml
###< squizlabs/php_codesniffer ###

# Generated test data files.
/tests/data/Speech_bubbles.png

3 changes: 3 additions & 0 deletions .travis.yml
Expand Up @@ -5,6 +5,7 @@ php:

before_install:
- npm -g install npm # Upgrade npm so we can use the ci install command.
- sudo apt-get install -y librsvg2-bin

install:
- composer install
Expand All @@ -15,7 +16,9 @@ script:
- node_modules/.bin/encore production
- git status
- git status | grep "nothing to commit, working tree clean"
# Run linting.
- composer lint
# Run tests.
- composer test

after_success:
Expand Down
2 changes: 2 additions & 0 deletions bin/dockerstart
Expand Up @@ -7,6 +7,8 @@ if [ -z $TOOLFORGE_DOCKER_PORT ]; then
fi

# Install dependencies.
apt-get update
apt-get install librsvg2-bin
composer install -d /var/www || exit 1;

# Start the Lighttpd web server.
Expand Down
9 changes: 7 additions & 2 deletions composer.json
Expand Up @@ -68,10 +68,15 @@
"assets:install %PUBLIC_DIR%": "symfony-cmd"
},
"post-install-cmd": [
"@auto-scripts"
"@auto-scripts",
"@generate-test-data"
],
"post-update-cmd": [
"@auto-scripts"
"@auto-scripts",
"@generate-test-data"
],
"generate-test-data": [
"LANG=de rsvg-convert ./tests/data/Speech_bubbles.svg > ./tests/data/Speech_bubbles.png"
],
"lint": [
"composer validate",
Expand Down
4 changes: 4 additions & 0 deletions config/services.yaml
Expand Up @@ -34,3 +34,7 @@ services:
App\Service\FileCache:
arguments:
$directory: '%kernel.project_dir%/var/cache/images'

App\Service\Renderer:
arguments:
$rsvgCommand: '/usr/bin/rsvg-convert'
51 changes: 33 additions & 18 deletions src/Controller/ApiController.php
Expand Up @@ -6,6 +6,7 @@
use App\Model\Svg\SvgFile;
use App\Model\Title;
use App\Service\FileCache;
use App\Service\Renderer;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
Expand All @@ -16,47 +17,59 @@
*/
class ApiController extends AbstractController
{

/** @var FileCache */
private $cache;

public function __construct(FileCache $cache)
/** @var Renderer */
protected $svgRenderer;

public function __construct(FileCache $cache, Renderer $svgRenderer)
{
$this->cache = $cache;
$this->svgRenderer = $svgRenderer;
}

/**
* @Route("/api/file/{fileName}", name="api_file", methods="GET")
* Serve a PNG rendering of the given SVG in the given language.
* @Route("/api/file/{filename}/{lang}.png", name="api_file", methods="GET")
*
* @param string $fileName
* @param string $filename
* @return Response
*/
public function getFile(string $fileName): Response
public function getFile(string $filename, string $lang): Response
{
$fileName = Title::normalize($fileName);
$content = $this->cache->getContent($fileName);
return $this->serveContent($content);
$filename = Title::normalize($filename);
return $this->serveContent($this->cache->getPath($filename), $lang);
}

/**
* @Route("/api/file/{fileName}", name="api_file_translated", methods="POST")
* @param string $fileName
* Serve a PNG rendering of the given SVG in the given language, based on the POSTed translations.
* @Route("/api/file/{filename}/{lang}.png", name="api_file_translated", methods="POST")
* @param string $filename
* @param Request $request
* @return Response
*/
public function getFileWithTranslations(string $fileName, Request $request): Response
public function getFileWithTranslations(string $filename, string $lang, Request $request): Response
{
$fileName = Title::normalize($fileName);
$filename = Title::normalize($filename);
$json = $request->getContent();
if ('' === $json) {
return $this->getFile($fileName);
return $this->getFile($filename, $lang);
}

$translations = \GuzzleHttp\json_decode($json, true);
$path = $this->cache->getPath($fileName);
$path = $this->cache->getPath($filename);
$file = new SvgFile($path);
$file->switchToTranslationSet($translations);

return $this->serveContent($file->saveToString());
// Write SVG file to filesystem, so it can be converted by rsvg. Use a unique filename
// because multiple users can be translating the same file at the same time (whereas
// getFile() above only ever serves the one version of a file).
$tmpSvgFilename = $this->cache->getTempSvgFile();
$file->saveToPath($tmpSvgFilename);

return $this->serveContent($tmpSvgFilename, $lang);
}

/**
Expand Down Expand Up @@ -92,15 +105,17 @@ public function getLanguages(string $fileName): Response
}

/**
* Serves an SVG
* Serves an SVG as a PNG.
*
* @param string $content
* @param string $svgFilename Full path to the SVG file to convert and serve as a PNG.
* @param string $lang The language to use for the SVG's text.
* @return Response
*/
private function serveContent(string $content): Response
private function serveContent(string $svgFilename, string $lang): Response
{
$content = $this->svgRenderer->render($svgFilename, $lang);
return new Response($content, 200, [
'Content-Type' => 'image/svg+xml',
'Content-Type' => 'image/png',
'X-File-Hash' => sha1($content),
]);
}
Expand Down
1 change: 1 addition & 0 deletions src/Controller/TranslateController.php
Expand Up @@ -137,6 +137,7 @@ public function translate(Request $request, Intuition $intuition, Session $sessi
'upload_button' => $uploadButton,
'language_selectors' => $languageSelectorsLayout,
'translations' => $translations,
'target_lang' => $targetLangDefault,
]);
}

Expand Down
10 changes: 10 additions & 0 deletions src/Service/FileCache.php
Expand Up @@ -107,4 +107,14 @@ protected function fullPath(string $fileName): string
{
return $this->directory.DIRECTORY_SEPARATOR.$fileName;
}

/**
* Create a file with a unique .svg filename, with access permission set to 0600,
* and return its name.
* @return string
*/
public function getTempSvgFile(): string
{
return tempnam($this->directory, 'temp_').'.svg';
}
}
43 changes: 43 additions & 0 deletions src/Service/Renderer.php
@@ -0,0 +1,43 @@
<?php
declare(strict_types = 1);

namespace App\Service;

use Symfony\Component\Process\Exception\ProcessFailedException;
use Symfony\Component\Process\Process;

/**
* SVG to PNG rendering service.
*/
class Renderer
{

/** @var string */
protected $rsvgCommand;

/**
* @param string $rsvgCommand The command to execute to do the conversion.
*/
public function __construct(string $rsvgCommand)
{
$this->rsvgCommand = $rsvgCommand;
}

/**
* @param string $file Full filesystem path to the SVG file to render.
* @param string $lang Code of the language in which to render the image.
* @throws ProcessFailedException If the PNG conversion failed.
* @return string The PNG image contents.
*/
public function render(string $file, string $lang) : string
{
$process = new Process([$this->rsvgCommand, $file]);
// Set the LANG environment variable, which will be interpreted as the SVG systemLanguage.
$process->setEnv(['LANG' => $lang]);
$process->run();
if (!$process->isSuccessful()) {
throw new ProcessFailedException($process);
}
return $process->getOutput();
}
}
2 changes: 1 addition & 1 deletion templates/translate.html.twig
Expand Up @@ -29,7 +29,7 @@
{{ download_button|raw }}
</div>
<div class="image">
<img src="{{ path('api_file', {fileName: filename}) }}" alt="{{ msg('translation-image-alt') }}" />
<img src="{{ path('api_file', {filename: filename, lang: target_lang}) }}" alt="{{ msg('translation-image-alt') }}" />
</div>
</div>
</form>
Expand Down
13 changes: 7 additions & 6 deletions tests/Controller/ApiControllerTest.php
Expand Up @@ -5,25 +5,26 @@

use App\Controller\ApiController;
use App\Service\FileCache;
use App\Service\Renderer;
use App\Tests\Model\Svg\SvgFileTest;
use PHPUnit\Framework\TestCase;
use Psr\Container\ContainerInterface;
use Symfony\Component\HttpFoundation\Request;

class ApiControllerTest extends TestCase
{

/**
* @covers \App\Controller\ApiController::getFile()
* @covers \App\Controller\ApiController::serveContent()
*/
public function testGetFile(): void
{
$controller = $this->makeController();

$response = $controller->getFile('file: foo.svg');
$response = $controller->getFile('file: foo.svg', 'de');
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('image/svg+xml', $response->headers->get('Content-Type'));
self::assertEquals('test content', $response->getContent());
self::assertEquals('image/png', $response->headers->get('Content-Type'));
self::assertStringEqualsFile(SvgFileTest::TEST_FILE_RENDERED, $response->getContent());
}

/**
Expand All @@ -43,7 +44,7 @@ public function testGetFileWithTranslations(): void
];
$request = new Request([], [], [], [], [], [], \GuzzleHttp\json_encode($translations));

$response = $controller->getFileWithTranslations('Foo.svg', $request);
$response = $controller->getFileWithTranslations('Foo.svg', 'ru', $request);
self::assertEquals(200, $response->getStatusCode());
self::assertEquals('image/svg+xml', $response->headers->get('Content-Type'));
self::assertNotFalse(strpos($response->getContent(), 'Прив1ет'));
Expand Down Expand Up @@ -99,7 +100,7 @@ private function makeController(): ApiController
->willReturn('test content');

/** @var FileCache $cache */
$controller = new ApiController($cache);
$controller = new ApiController($cache, new Renderer('rsvg-convert'));
$controller->setContainer($container);

return $controller;
Expand Down
6 changes: 6 additions & 0 deletions tests/Model/Svg/SvgFileTest.php
Expand Up @@ -21,6 +21,12 @@ class SvgFileTest extends TestCase
{
public const TEST_FILE = __DIR__.'/../../data/Speech_bubbles.svg';

/**
* This PNG file is generated as part of the Composer installation process,
* because rsvg is not deterministic.
*/
public const TEST_FILE_RENDERED = __DIR__.'/../../data/Speech_bubbles.png';

public const EXPECTED_TRANSLATIONS = [
'tspan2987' =>
[
Expand Down
20 changes: 20 additions & 0 deletions tests/Service/RendererTest.php
@@ -0,0 +1,20 @@
<?php
declare(strict_types = 1);

namespace App\Tests\Service;

use App\Service\Renderer;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Process\Exception\ProcessFailedException;

class RendererTest extends TestCase
{

public function testInvalidCommand() : void
{
$renderer = new Renderer('foo');
static::expectException(ProcessFailedException::class);
static::expectExceptionMessage('foo: not found');
$renderer->render('foo.svg', 'fr');
}
}

0 comments on commit baabcb0

Please sign in to comment.