Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion src/DiagramGenerator/Board.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use DiagramGenerator\Config;
use DiagramGenerator\Fen;
use DiagramGenerator\Image\Storage;
use DiagramGenerator\Image\StorageNew;
use DiagramGenerator\Image\Image;

/**
Expand Down Expand Up @@ -76,7 +77,11 @@ public function getImage()
*/
protected function generateImage()
{
$storage = new Storage($this->cacheDir, $this->pieceThemeUrl, $this->boardTextureUrl);
if ($this->config->hasThemeUrls()) {
$storage = new StorageNew($this->cacheDir, $this->pieceThemeUrl, $this->boardTextureUrl);
} else {
$storage = new Storage($this->cacheDir, $this->pieceThemeUrl, $this->boardTextureUrl);
}
$image = new Image($storage, $this->config);
$topPadding = $storage->getMaxPieceHeight($this->fen, $this->config) - $this->config->getSize()->getCell();

Expand Down
44 changes: 44 additions & 0 deletions src/DiagramGenerator/Config.php
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ class Config
#[Range(min: 0, max: 100)]
protected $compressionQualityJpg;

/**
* Custom theme URLs object with board and piece URLs.
* Structure: { board: boardUrl, wp: pieceUrl, bp: pieceUrl, ... }
*
* @var array|null
*/
#[Type('array')]
protected $themeUrls;

/**
* Gets the value of fen.
*
Expand Down Expand Up @@ -454,6 +463,41 @@ public function setCompressionQualityJpg($compressionQualityJpg)
return $this;
}

/**
* Gets the theme URLs object.
*
* @return array|null
*/
public function getThemeUrls()
{
return $this->themeUrls;
}

/**
* Sets the theme URLs object.
* Structure: { board: boardUrl, wp: pieceUrl, bp: pieceUrl, ... }
*
* @param array|null $themeUrls
*
* @return self
*/
public function setThemeUrls($themeUrls)
{
$this->themeUrls = $themeUrls;

return $this;
}

/**
* Checks if custom theme URLs are configured.
*
* @return bool
*/
public function hasThemeUrls()
{
return !empty($this->themeUrls) && is_array($this->themeUrls);
}

public function getBorderThickness()
{
return (int) round($this->getSize()->getCell() / 2);
Expand Down
13 changes: 10 additions & 3 deletions src/DiagramGenerator/Image/Image.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,17 @@ class Image
/** @var BaseImage */
protected $image;

/** @var Storage */
/** @var Storage|StorageNew */
protected $storage;

/** @var Config */
protected $config;

public function __construct(Storage $storage, Config $config)
/**
* @param Storage|StorageNew $storage
* @param Config $config
*/
public function __construct($storage, Config $config)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Any reason type hint is removed from storage?

Suggested change
public function __construct($storage, Config $config)
public function __construct(StorageInterface $storage, Config $config)

{
$this->image = (new Decoder())->initFromGdResource(imagecreatetruecolor(1, 1));
$this->storage = $storage;
Expand Down Expand Up @@ -133,13 +137,16 @@ public function drawBoardWithFigures(Fen $fen, $cellSize, $topPaddingOfCell)
{
$this->image = $this->drawBoard($this->storage->getBackgroundTextureImage($this->config), $cellSize, $topPaddingOfCell);

$boardHasTexture = !empty($this->config->getTexture()) ||
($this->config->hasThemeUrls() && isset($this->config->getThemeUrls()['board']));

$this->drawCells(
$this->config->getSize()->getCell(),
$this->config->getDark(),
$this->config->getLight(),
$this->config->getHighlightSquaresColor(),
$topPaddingOfCell,
!empty($this->config->getTexture()),
$boardHasTexture,
$this->config->getHighlightSquares()
);

Expand Down
273 changes: 273 additions & 0 deletions src/DiagramGenerator/Image/StorageNew.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
<?php

namespace DiagramGenerator\Image;

use DiagramGenerator\Config;
use DiagramGenerator\Config\Texture;
use DiagramGenerator\Fen;
use DiagramGenerator\Fen\Piece;
use Intervention\Image\Exception\NotReadableException;
use Intervention\Image\Image;
use Intervention\Image\ImageManagerStatic;
use RuntimeException;

class StorageNew
{
protected $pieces = [];

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

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

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

/**
* @param string $cacheDirectory
* @param string $pieceThemeUrl
* @param string $boardTextureUrl
*/
public function __construct($cacheDirectory, $pieceThemeUrl, $boardTextureUrl)
{
$this->cacheDirectory = $cacheDirectory;
$this->pieceThemeUrl = $pieceThemeUrl;
$this->boardTextureUrl = $boardTextureUrl;
}

/**
*
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method documentation is incomplete. It should include @param Piece $piece and @param Config $config to document the parameters, following the PHPDoc standard used in the codebase.

Suggested change
*
*
* @param Piece $piece
* @param Config $config

Copilot uses AI. Check for mistakes.
* @return Image
*/
public function getPieceImage(Piece $piece, Config $config)
{
return $this->getPieceImageFromTheme($piece, $config);
}

/**
* Gets piece image from theme URLs.
*
* @return Image
*/
protected function getPieceImageFromTheme(Piece $piece, Config $config)
{
$themeUrls = $config->getThemeUrls();
$pieceShortName = $piece->getShortName();

if (!isset($themeUrls[$pieceShortName])) {
throw new RuntimeException(sprintf('Piece URL not found in theme for piece: %s', $pieceShortName));
}

$pieceUrl = $themeUrls[$pieceShortName];
$cacheKey = $pieceUrl;

if (!isset($this->pieces[$cacheKey])) {
$this->pieces[$cacheKey] = $this->fetchRemotePieceImageFromTheme($piece, $config);
}

return $this->pieces[$cacheKey];
}

/**
* @return Image|null
*/
public function getBackgroundTextureImage(Config $config)
{
return $this->getBackgroundTextureImageFromTheme($config);
}

/**
* Gets background texture image from theme URL.
*
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing @param Config $config parameter documentation in the PHPDoc comment.

Suggested change
*
*
* @param Config $config

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Useless, it's already in the code.

* @return Image|null
*/
protected function getBackgroundTextureImageFromTheme(Config $config)
{
$themeUrls = $config->getThemeUrls();

if (!isset($themeUrls['board'])) {
return null;
}

$boardUrl = $themeUrls['board'];
$boardCachedPath = $this->getCachedTextureFilePathFromTheme($boardUrl);

try {
return ImageManagerStatic::make($boardCachedPath);
} catch (NotReadableException $exception) {
@mkdir(dirname($boardCachedPath), 0777, true);
$this->cacheImage($boardUrl, $boardCachedPath);
return ImageManagerStatic::make($boardCachedPath);
}
}

/**
* Finds max height of piece image
*
*
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parameter documentation. Should include @param Fen $fen and @param Config $config in the PHPDoc comment.

Suggested change
*
* @param Fen $fen
* @param Config $config

Copilot uses AI. Check for mistakes.
* @return int
*/
public function getMaxPieceHeight(Fen $fen, Config $config)
Comment thread
pavoljurov marked this conversation as resolved.
Outdated
{
$maxHeight = $config->getSize()->getCell();
foreach ($fen->getPieces() as $piece) {
$pieceImage = $this->getPieceImage($piece, $config);

if ($pieceImage->getHeight() > $maxHeight) {
$maxHeight = $pieceImage->getHeight();
}

unset($pieceImage);
}

return $maxHeight;
}

/**
* Fetches piece image from theme URL.
*
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parameter documentation. Should include @param Piece $piece and @param Config $config in the PHPDoc comment.

Suggested change
*
*
* @param Piece $piece
* @param Config $config

Copilot uses AI. Check for mistakes.
* @return Image
*/
protected function fetchRemotePieceImageFromTheme(Piece $piece, Config $config)
Comment thread
pavoljurov marked this conversation as resolved.
Outdated
{
$themeUrls = $config->getThemeUrls();
$pieceShortName = $piece->getShortName();

if (!isset($themeUrls[$pieceShortName])) {
throw new RuntimeException(sprintf('Piece URL not found in theme for piece: %s', $pieceShortName));
}

$pieceUrl = $themeUrls[$pieceShortName];
$pieceCachedPath = $this->getCachedPieceFilePathFromTheme($pieceUrl, $pieceShortName);

try {
$image = ImageManagerStatic::make($pieceCachedPath);
} catch (NotReadableException $exception) {
$this->downloadPieceImagesFromTheme($config);
$image = ImageManagerStatic::make($pieceCachedPath);
}

return $image;
}

/**
* Downloads all piece images from theme URLs.
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing parameter documentation. Should include @param Config $config in the PHPDoc comment.

Suggested change
* Downloads all piece images from theme URLs.
* Downloads all piece images from theme URLs.
*
* @param Config $config

Copilot uses AI. Check for mistakes.
*/
private function downloadPieceImagesFromTheme(Config $config)
Comment thread
pavoljurov marked this conversation as resolved.
Outdated
{
$themeUrls = $config->getThemeUrls();
$pieces = Piece::generateAllPieces();

$handles = [];
$fileHandles = [];
$multiHandle = curl_multi_init();

foreach ($pieces as $piece) {
$pieceShortName = $piece->getShortName();

if (!isset($themeUrls[$pieceShortName])) {
continue; // Skip pieces without URLs
}

$pieceUrl = $themeUrls[$pieceShortName];
$filePath = $this->getCachedPieceFilePathFromTheme($pieceUrl, $pieceShortName);
@mkdir(dirname($filePath), 0777, true);

$uniqid = uniqid();
$handles[$pieceShortName] = curl_init($pieceUrl);
$fileHandles[$pieceShortName] = [
'handle' => fopen($filePath . $uniqid, 'wb'),
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If fopen() fails and returns false, it should be handled before attempting to use the file handle. Add error handling similar to the pattern in cacheImage() method (lines 261-263):

$handle = fopen($filePath . $uniqid, 'wb');
if (!$handle) {
    throw new RuntimeException(sprintf('Could not open temporary file: %s', $filePath . $uniqid));
}
$fileHandles[$pieceShortName] = [
    'handle' => $handle,
    // ...
];
Suggested change
$fileHandles[$pieceShortName] = [
'handle' => fopen($filePath . $uniqid, 'wb'),
$handle = fopen($filePath . $uniqid, 'wb');
if (!$handle) {
throw new RuntimeException(sprintf('Could not open temporary file: %s', $filePath . $uniqid));
}
$fileHandles[$pieceShortName] = [
'handle' => $handle,

Copilot uses AI. Check for mistakes.
'tmpPath' => $filePath . $uniqid,
'realPath' => $filePath,
];
}

foreach($handles as $key => $handle) {
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after foreach keyword. This should be foreach ($handles (with a space) to maintain consistency with the coding style used elsewhere in the codebase.

Suggested change
foreach($handles as $key => $handle) {
foreach ($handles as $key => $handle) {

Copilot uses AI. Check for mistakes.
curl_setopt($handle, CURLOPT_FILE, $fileHandles[$key]['handle']);
curl_setopt($handle, CURLOPT_HEADER, 0);

curl_multi_add_handle($multiHandle, $handle);
}
Comment on lines +164 to +169
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cURL handle operations in the loop don't check for errors. Consider checking if curl_init() returns false and handling that case, or at minimum checking for errors after curl_multi_exec() to ensure downloads completed successfully.

Copilot uses AI. Check for mistakes.

do {
curl_multi_exec($multiHandle, $running);
curl_multi_select($multiHandle);
} while ($running > 0);

foreach ($fileHandles as $fileHandle) {
if (isset($fileHandle['tmpPath']) && file_exists($fileHandle['tmpPath'])) {
rename($fileHandle['tmpPath'], $fileHandle['realPath']);
}
}
Comment on lines +176 to +180
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File handles should be closed before attempting to rename the files. On Windows systems, attempting to rename an open file may fail. The file handles in $fileHandles should be closed in this loop before the rename() call, not later in the cleanup section.

Copilot uses AI. Check for mistakes.

Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

File handles opened with fopen() at line 180 are never closed. After the curl operations complete and files are renamed, the file handles should be closed with fclose() to prevent resource leaks. Add a loop after line 202 to close all file handles:

foreach ($fileHandles as $fileHandle) {
    if (is_resource($fileHandle['handle'])) {
        fclose($fileHandle['handle']);
    }
}
Suggested change
// Close all file handles to prevent resource leaks
foreach ($fileHandles as $fileHandle) {
if (is_resource($fileHandle['handle'])) {
fclose($fileHandle['handle']);
}
}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The curl_init() handles created at line 178 are never individually closed with curl_close() before calling curl_multi_close(). While curl_multi_close() may clean up multi handle resources, the individual curl handles should be explicitly removed and closed. Add cleanup after line 196:

foreach ($handles as $handle) {
    curl_multi_remove_handle($multiHandle, $handle);
    curl_close($handle);
}
Suggested change
// Cleanup: remove and close each cURL handle
foreach ($handles as $handle) {
curl_multi_remove_handle($multiHandle, $handle);
curl_close($handle);
}

Copilot uses AI. Check for mistakes.
curl_multi_close($multiHandle);
}

/**
* Gets cached piece file path for theme URLs.
* Uses the URL to generate a unique cache key.
*
* @param string $pieceUrl The URL of the piece image
* @param string $piece The piece short name (e.g., 'wp', 'bk')
* @return string
*/
protected function getCachedPieceFilePathFromTheme($pieceUrl, $piece)
{
$urlHash = md5($pieceUrl);
$extension = pathinfo(parse_url($pieceUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: Texture::IMAGE_FORMAT_PNG;

return sprintf(
'%s/theme/%s/%s.%s',
$this->cacheDirectory,
$urlHash,
$piece,
$extension
);
}

/**
* Gets cached texture file path for theme URLs.
* Uses the URL to generate a unique cache key.
*
* @param string $boardUrl The URL of the board image
* @return string
*/
protected function getCachedTextureFilePathFromTheme($boardUrl)
{
$urlHash = md5($boardUrl);
$extension = pathinfo(parse_url($boardUrl, PHP_URL_PATH), PATHINFO_EXTENSION) ?: Texture::IMAGE_FORMAT_PNG;

return sprintf(
'%s/board/theme/%s.%s',
$this->cacheDirectory,
$urlHash,
$extension
);
}

/**
* Fetches remove file, and stores it locally
Copy link

Copilot AI Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in comment: "remove" should be "remote".

Suggested change
* Fetches remove file, and stores it locally
* Fetches remote file, and stores it locally

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo in the comment: "remove" should be "remote".

Suggested change
* Fetches remove file, and stores it locally
* Fetches remote file, and stores it locally

Copilot uses AI. Check for mistakes.
*
* @param $remoteImageUrl
* @param $cachedFilePath
*/
protected function cacheImage($remoteImageUrl, $cachedFilePath)
{
$cachedFilePathTmp = $cachedFilePath.uniqid('', true);
$ch = curl_init($remoteImageUrl);
$destinationFileHandle = fopen($cachedFilePathTmp, 'wb');

if (!$destinationFileHandle) {
throw new RuntimeException(sprintf('Could not open temporary file: %s', $cachedFilePathTmp));
}

curl_setopt($ch, CURLOPT_FILE, $destinationFileHandle);
curl_setopt($ch, CURLOPT_HEADER, 0);
curl_exec($ch);
curl_close($ch);
Comment on lines +254 to +257
Copy link

Copilot AI Nov 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The cURL execution result is not checked. If curl_exec() fails, it returns false, but this is not validated. Consider checking the result and throwing an exception if the download fails, similar to how file handle creation is handled above.

Copilot uses AI. Check for mistakes.
fclose($destinationFileHandle);

rename($cachedFilePathTmp, $cachedFilePath);
}
}
Loading