Initial commit
This commit is contained in:
commit
a437c068c8
64 changed files with 561683 additions and 0 deletions
87
src/Controller/DFG3dController.php
Normal file
87
src/Controller/DFG3dController.php
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Controller;
|
||||
|
||||
use Drupal\wisski_core\Entity\WisskiEntity;
|
||||
use Drupal\Core\Entity\ContentEntityStorageInterface;
|
||||
use Drupal\Core\Cache\CacheableJsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
use Drupal\image\Entity\ImageStyle;
|
||||
use Drupal\wisski_salz\Entity\Adapter;
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
|
||||
|
||||
class DFG3dController extends ControllerBase {
|
||||
|
||||
|
||||
public function __construct() {
|
||||
dfg_3dviewer_init_constants();
|
||||
}
|
||||
|
||||
public static function create($container) {
|
||||
return new static();
|
||||
}
|
||||
|
||||
public function view() {
|
||||
return [
|
||||
'#markup' => DFG_3DVIEWER_MAIN_URL,
|
||||
];
|
||||
}
|
||||
|
||||
public function editEntity(?WisskiEntity $wisski_individual = NULL) {
|
||||
|
||||
$js_settings = dfg_3dviewer_build_js_settings();
|
||||
$pathGeneration = $js_settings['viewer']['imageGeneration'] ?? NULL;
|
||||
|
||||
if ($pathGeneration) {
|
||||
|
||||
$url = \Drupal::request()->request->get('path');
|
||||
|
||||
// Change URL to public://
|
||||
$parsed = parse_url($url, PHP_URL_PATH);
|
||||
|
||||
if (str_starts_with($parsed, '/sites/default/files/')) {
|
||||
|
||||
$relative = str_replace('/sites/default/files/', '', $parsed);
|
||||
$uri = 'public://' . $relative;
|
||||
|
||||
$realpath = \Drupal::service('file_system')->realpath($uri);
|
||||
|
||||
if (file_exists($realpath)) {
|
||||
|
||||
// Check wheter file exists or not
|
||||
$files = \Drupal::entityTypeManager()
|
||||
->getStorage('file')
|
||||
->loadByProperties(['uri' => $uri]);
|
||||
|
||||
if ($files) {
|
||||
$file = reset($files);
|
||||
} else {
|
||||
$file = File::create(['uri' => $uri]);
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
}
|
||||
|
||||
$wisski_individual->set($pathGeneration, [
|
||||
'target_id' => $file->id(),
|
||||
]);
|
||||
|
||||
$wisski_individual->save();
|
||||
}
|
||||
}
|
||||
|
||||
$response = new CacheableJsonResponse();
|
||||
$response->setEncodingOptions(JSON_UNESCAPED_SLASHES);
|
||||
|
||||
$response->setData([
|
||||
"id" => $wisski_individual->id(),
|
||||
"path" => $url,
|
||||
]);
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
}
|
||||
99
src/Controller/ModelController.php
Normal file
99
src/Controller/ModelController.php
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Controller;
|
||||
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Drupal\node\Entity\Node;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
|
||||
class ModelController {
|
||||
|
||||
public function create(Request $request) {
|
||||
|
||||
$node = Node::create([
|
||||
'type' => 'model',
|
||||
'title' => 'Processing model',
|
||||
'field_processing_progress' => 0,
|
||||
'field_processing_status' => 'preparing',
|
||||
'field_processing_message' => 'Preparing...'
|
||||
]);
|
||||
|
||||
$node->save();
|
||||
|
||||
$id = $node->id();
|
||||
|
||||
exec("/opt/drupal/scripts/worker.sh $id > /dev/null 2>&1 &");
|
||||
|
||||
return new JsonResponse([
|
||||
'entity_id' => $id,
|
||||
'status' => 'started'
|
||||
]);
|
||||
}
|
||||
|
||||
public function status($id) {
|
||||
$entity = $this->loadProcessingEntity((string) $id);
|
||||
if (!$entity) {
|
||||
return new JsonResponse(['error' => 'not found'], 404);
|
||||
}
|
||||
|
||||
$progress = 0;
|
||||
$status = 'unknown';
|
||||
$message = '';
|
||||
|
||||
if ($entity->hasField('field_processing_progress')) {
|
||||
$value = $entity->get('field_processing_progress')->value;
|
||||
$progress = is_numeric($value) ? (int) $value : 0;
|
||||
}
|
||||
if ($entity->hasField('field_processing_status')) {
|
||||
$statusValue = $entity->get('field_processing_status')->value;
|
||||
$status = $statusValue !== NULL && $statusValue !== '' ? (string) $statusValue : 'unknown';
|
||||
}
|
||||
if ($entity->hasField('field_processing_message')) {
|
||||
$messageValue = $entity->get('field_processing_message')->value;
|
||||
$message = $messageValue !== NULL ? (string) $messageValue : '';
|
||||
}
|
||||
|
||||
$response = new JsonResponse([
|
||||
'progress' => $progress,
|
||||
'status' => $status,
|
||||
'message' => $message
|
||||
]);
|
||||
|
||||
$response->headers->set('Cache-Control','no-store');
|
||||
|
||||
return $response;
|
||||
|
||||
}
|
||||
|
||||
private function loadProcessingEntity(string $id): ?EntityInterface {
|
||||
if ($id === '') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$entity_type_manager = \Drupal::entityTypeManager();
|
||||
|
||||
foreach (['wisski_individual', 'node'] as $entity_type) {
|
||||
try {
|
||||
if ($entity_type_manager->hasDefinition($entity_type)) {
|
||||
$entity = $entity_type_manager->getStorage($entity_type)->load($id);
|
||||
if ($entity instanceof EntityInterface) {
|
||||
return $entity;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
}
|
||||
}
|
||||
|
||||
if (ctype_digit($id)) {
|
||||
$node = Node::load((int) $id);
|
||||
if ($node instanceof EntityInterface) {
|
||||
return $node;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
105
src/Controller/SaveMetadataController.php
Normal file
105
src/Controller/SaveMetadataController.php
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
|
||||
class SaveMetadataController extends ControllerBase {
|
||||
|
||||
private const ALLOWED_SUBDIR = 'viewer';
|
||||
|
||||
public function save(Request $request): JsonResponse {
|
||||
|
||||
/* =========================
|
||||
Auth (no $_SESSION)
|
||||
========================= */
|
||||
|
||||
if ($this->currentUser()->isAnonymous()) {
|
||||
throw new AccessDeniedHttpException('Unauthorized');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Input JSON
|
||||
========================= */
|
||||
|
||||
$input = json_decode($request->getContent(), true);
|
||||
if (!is_array($input)) {
|
||||
throw new BadRequestHttpException('Invalid JSON');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Validate data
|
||||
========================= */
|
||||
|
||||
$path = $input['path'] ?? '';
|
||||
$filename = $input['filename'] ?? '';
|
||||
$content = $input['content'] ?? '';
|
||||
|
||||
if (!is_string($path) || !is_string($filename)) {
|
||||
throw new BadRequestHttpException('Invalid input');
|
||||
}
|
||||
|
||||
if (!preg_match('#^[a-zA-Z0-9._-]+(?:/[a-zA-Z0-9._-]+)*$#', $path)) {
|
||||
\Drupal::logger('dfg_3dviewer')->error(
|
||||
'Invalid path received: "@path"',
|
||||
['@path' => $path]
|
||||
);
|
||||
throw new BadRequestHttpException('Invalid path');
|
||||
}
|
||||
|
||||
if (!preg_match('#^[a-zA-Z0-9._-]+$#', $filename)) {
|
||||
\Drupal::logger('dfg_3dviewer')->error(
|
||||
'Invalid filename received: "@filename"',
|
||||
['@filename' => $filename]
|
||||
);
|
||||
throw new BadRequestHttpException('Invalid filename');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Paths (safe)
|
||||
========================= */
|
||||
|
||||
$directory = 'public://' . $path . '/metadata';
|
||||
|
||||
$fileSystem = \Drupal::service('file_system');
|
||||
|
||||
$fileSystem->prepareDirectory(
|
||||
$directory,
|
||||
FileSystemInterface::CREATE_DIRECTORY |
|
||||
FileSystemInterface::MODIFY_PERMISSIONS
|
||||
);
|
||||
|
||||
$realDir = $fileSystem->realpath($directory);
|
||||
|
||||
/* =========================
|
||||
Save file
|
||||
========================= */
|
||||
|
||||
$content = is_array($content)
|
||||
? json_encode($content, JSON_PRETTY_PRINT)
|
||||
: (string) $content;
|
||||
|
||||
$filePath = $directory . '/' . $filename . '_viewer.json';
|
||||
|
||||
$fileSystem->saveData(
|
||||
$content,
|
||||
$filePath,
|
||||
FileSystemInterface::EXISTS_REPLACE
|
||||
);
|
||||
|
||||
/* =========================
|
||||
Response
|
||||
========================= */
|
||||
|
||||
return new JsonResponse([
|
||||
'status' => 'ok',
|
||||
]);
|
||||
}
|
||||
}
|
||||
169
src/Controller/ThumbnailUploadController.php
Normal file
169
src/Controller/ThumbnailUploadController.php
Normal file
|
|
@ -0,0 +1,169 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
|
||||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||||
|
||||
class ThumbnailUploadController extends ControllerBase {
|
||||
|
||||
private function getConfiguredBaseUrl(): string {
|
||||
$config = \Drupal::config('dfg_3dviewer.settings');
|
||||
$base = (string) (
|
||||
$config->get('dfg_3dviewer_main_url')
|
||||
?? $config->get('main_url')
|
||||
?? ''
|
||||
);
|
||||
$base = trim($base);
|
||||
if ($base === '') {
|
||||
$request = \Drupal::request();
|
||||
return rtrim($request->getSchemeAndHttpHost(), '/');
|
||||
}
|
||||
if (!preg_match('#^https?://#i', $base)) {
|
||||
$base = 'https://' . $base;
|
||||
}
|
||||
return rtrim($base, '/');
|
||||
}
|
||||
|
||||
public function upload(Request $request): JsonResponse {
|
||||
|
||||
/* =========================
|
||||
Auth
|
||||
========================= */
|
||||
|
||||
if ($this->currentUser()->isAnonymous()) {
|
||||
throw new AccessDeniedHttpException('Unauthorized');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Input (POST fields)
|
||||
========================= */
|
||||
|
||||
$filename = $request->request->get('filename', '');
|
||||
$wisskiId = $request->request->get('wisski_individual', '');
|
||||
|
||||
if (!is_string($filename) || !preg_match('#^[a-zA-Z0-9_-]+$#', $filename)) {
|
||||
throw new BadRequestHttpException('Invalid filename');
|
||||
}
|
||||
|
||||
if (!is_string($wisskiId) || !preg_match('#^[a-zA-Z0-9_-]+$#', $wisskiId)) {
|
||||
throw new BadRequestHttpException('Invalid WissKI id');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Uploaded file
|
||||
========================= */
|
||||
|
||||
$file = $request->files->get('data');
|
||||
|
||||
if (!$file || $file->getError() !== UPLOAD_ERR_OK) {
|
||||
throw new BadRequestHttpException('Upload failed');
|
||||
}
|
||||
|
||||
$allowed = ['image/png', 'image/jpeg'];
|
||||
|
||||
$mime = $file->getClientMimeType();
|
||||
|
||||
if (!in_array($mime, $allowed, true)) {
|
||||
throw new HttpException(415, 'Invalid image type');
|
||||
}
|
||||
|
||||
if (!@getimagesize($file->getPathname())) {
|
||||
throw new HttpException(415, 'File is not a valid image');
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Target directory
|
||||
========================= */
|
||||
|
||||
|
||||
$uri = (string) $request->request->get('path', '');
|
||||
$parsed = parse_url($uri);
|
||||
$path = $parsed['path'] ?? '';
|
||||
|
||||
$relative = preg_replace('#^/?sites/default/files/#', '', $path);
|
||||
|
||||
if (str_contains($relative, '..')) {
|
||||
throw new \InvalidArgumentException('Invalid path');
|
||||
}
|
||||
|
||||
$fileSystem = \Drupal::service('file_system');
|
||||
|
||||
$directory = 'public://' . $relative;
|
||||
$views = $directory . '/views';
|
||||
|
||||
if (!is_dir($fileSystem->realpath($directory))) {
|
||||
throw new \RuntimeException('Base directory missing');
|
||||
}
|
||||
|
||||
$fileSystem->prepareDirectory(
|
||||
$views,
|
||||
FileSystemInterface::CREATE_DIRECTORY
|
||||
| FileSystemInterface::MODIFY_PERMISSIONS
|
||||
);
|
||||
|
||||
/* =========================
|
||||
Filename
|
||||
========================= */
|
||||
|
||||
$filename = preg_replace('/[^a-zA-Z0-9_-]/', '_', $filename);
|
||||
|
||||
$extension = match ($mime) {
|
||||
'image/png' => 'png',
|
||||
'image/jpeg' => 'jpg',
|
||||
};
|
||||
|
||||
$targetName = $filename . '_side45.' . $extension;
|
||||
|
||||
/* =========================
|
||||
Save
|
||||
========================= */
|
||||
|
||||
$file->move(
|
||||
$fileSystem->realpath($views),
|
||||
$targetName
|
||||
);
|
||||
|
||||
$realPath = $fileSystem->realpath($views . '/' . $targetName);
|
||||
/* =========================
|
||||
WissKI call
|
||||
========================= */
|
||||
|
||||
$wisskiBaseUrl = $this->getConfiguredBaseUrl();
|
||||
$url = $wisskiBaseUrl . '/wisski/dfg_3dviewer/' . $wisskiId . '/savePreview';
|
||||
|
||||
$client = \Drupal::httpClient();
|
||||
|
||||
try {
|
||||
$response = $client->post($url, [
|
||||
'form_params' => [
|
||||
'path' => $realPath,
|
||||
],
|
||||
'timeout' => 5,
|
||||
]);
|
||||
|
||||
$wisskiStatus = $response->getStatusCode();
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
\Drupal::logger('dfg_3dviewer')->error($e->getMessage());
|
||||
$wisskiStatus = 0;
|
||||
}
|
||||
|
||||
/* =========================
|
||||
Response
|
||||
========================= */
|
||||
|
||||
return new JsonResponse([
|
||||
'status' => 'ok',
|
||||
'bytes' => filesize($realPath),
|
||||
'wisski_status' => $wisskiStatus
|
||||
]);
|
||||
}
|
||||
}
|
||||
956
src/Controller/XmlExportController.php
Normal file
956
src/Controller/XmlExportController.php
Normal file
|
|
@ -0,0 +1,956 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Controller;
|
||||
|
||||
use Drupal\Core\Controller\ControllerBase;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
class XmlExportController extends ControllerBase {
|
||||
|
||||
private const XSL_URL = 'https://raw.githubusercontent.com/slub/dfg-viewer/e54305a9fa58951d3f3d1dd7e64554cb2ee881eb/Resources/Public/XSLT/exportSingleToMetsMods.xsl';
|
||||
private const JSON_EXPORT_PATH = '/api/digital_reconstruction/record/%d';
|
||||
private const ADDITIONAL_MODEL_FIELD_CANDIDATES = [
|
||||
'fdc6300213a0d25d4b68069564846363',
|
||||
];
|
||||
private const EXPORT_PATHS = [
|
||||
'/wisski/navigate/%d/view',
|
||||
'/export_xml_single/%d',
|
||||
];
|
||||
private const FILE_DIR = 'public://xml_structure';
|
||||
|
||||
protected ClientInterface $httpClient;
|
||||
protected FileSystemInterface $fileSystem;
|
||||
|
||||
public function __construct(
|
||||
ClientInterface $http_client,
|
||||
FileSystemInterface $file_system
|
||||
) {
|
||||
$this->httpClient = $http_client;
|
||||
$this->fileSystem = $file_system;
|
||||
}
|
||||
|
||||
public static function create(ContainerInterface $container): self {
|
||||
return new static(
|
||||
$container->get('http_client'),
|
||||
$container->get('file_system')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Route callback.
|
||||
*/
|
||||
public function export(Request $request, ?string $id = null): Response {
|
||||
$content = trim((string) $request->getContent());
|
||||
$domain = null;
|
||||
|
||||
if ($request->isMethod('GET')) {
|
||||
$domain = trim((string) $request->query->get('domain', ''));
|
||||
}
|
||||
elseif ($content !== '' && $this->isJson($content)) {
|
||||
$data = json_decode($content, true);
|
||||
$id = $id ?? ($data['id'] ?? null);
|
||||
$domain = trim((string) ($data['domain'] ?? ''));
|
||||
if ($domain === '') {
|
||||
return new Response('Missing domain', 400);
|
||||
}
|
||||
}
|
||||
|
||||
if (!$id) {
|
||||
return new Response('Missing id', 400);
|
||||
}
|
||||
|
||||
if ($request->isMethod('GET') && !$domain) {
|
||||
return new Response('Missing domain', 400);
|
||||
}
|
||||
|
||||
try {
|
||||
$result = $this->buildExportXml($request, (int) $id, (string) $domain);
|
||||
$this->saveXml((string) $id, $result);
|
||||
|
||||
return new Response(
|
||||
$result,
|
||||
200,
|
||||
['Content-Type' => 'application/xml; charset=UTF-8']
|
||||
);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
\Drupal::logger('dfg_3dviewer')->error($e->getMessage());
|
||||
return new Response('XML export failed', 500);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected function isJson(string $content): bool {
|
||||
json_decode($content);
|
||||
return json_last_error() === JSON_ERROR_NONE;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Builds export XML from request body, legacy XML source or JSON API source.
|
||||
*/
|
||||
protected function buildExportXml(Request $request, int $id, string $domain): string {
|
||||
$xmlString = trim($request->getContent() ?? '');
|
||||
if ($xmlString !== '') {
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($xmlString);
|
||||
if ($xml instanceof \SimpleXMLElement) {
|
||||
return $this->transformXml($xml, $domain);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$xml = $this->fetchSourceXmlFromDomain($id, $domain);
|
||||
return $this->transformXml($xml, $domain);
|
||||
}
|
||||
catch (\Throwable $legacyException) {
|
||||
\Drupal::logger('dfg_3dviewer')->warning(
|
||||
'Legacy XML source fetch failed for @id, trying JSON fallback: @msg',
|
||||
[
|
||||
'@id' => (string) $id,
|
||||
'@msg' => $legacyException->getMessage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
$record = $this->fetchJsonRecordFromDomain($id, $domain);
|
||||
$record = $this->enrichJsonRecordFromLocalEntity($record, $id);
|
||||
return $this->buildXmlFromJsonRecord($record, $id, $domain);
|
||||
}
|
||||
|
||||
protected function fetchSourceXmlFromDomain(int $id, string $domain): \SimpleXMLElement {
|
||||
$domain = $this->normalizeDomain($domain);
|
||||
if ($domain === '') {
|
||||
throw new \RuntimeException('Missing domain for XML source fetch');
|
||||
}
|
||||
|
||||
$query = http_build_query(['page' => 0, '_format' => 'xml']);
|
||||
$attempts = [];
|
||||
|
||||
foreach (self::EXPORT_PATHS as $pattern) {
|
||||
$url = $domain . sprintf($pattern, $id) . '?' . $query;
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', $url, ['http_errors' => false]);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
$attempts[] = sprintf('%s => exception: %s', $url, $e->getMessage());
|
||||
continue;
|
||||
}
|
||||
|
||||
$status = $response->getStatusCode();
|
||||
$attempts[] = sprintf('%s => %d', $url, $status);
|
||||
if ($status !== 200) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$xmlString = (string) $response->getBody();
|
||||
if ($xmlString === '') {
|
||||
$attempts[] = sprintf('%s => empty body', $url);
|
||||
continue;
|
||||
}
|
||||
|
||||
libxml_use_internal_errors(true);
|
||||
$xml = simplexml_load_string($xmlString);
|
||||
if ($xml instanceof \SimpleXMLElement) {
|
||||
return $xml;
|
||||
}
|
||||
$attempts[] = sprintf('%s => invalid XML', $url);
|
||||
}
|
||||
|
||||
\Drupal::logger('dfg_3dviewer')->error('XML source fetch failed; attempts: @attempts', ['@attempts' => implode('; ', $attempts)]);
|
||||
throw new \RuntimeException('Cannot fetch source XML from domain');
|
||||
}
|
||||
|
||||
protected function normalizeDomain(string $domain): string {
|
||||
$domain = trim($domain);
|
||||
if ($domain === '') {
|
||||
return '';
|
||||
}
|
||||
if (!preg_match('#^https?://#i', $domain)) {
|
||||
$domain = 'https://' . $domain;
|
||||
}
|
||||
return rtrim($domain, '/');
|
||||
}
|
||||
|
||||
protected function fetchJsonRecordFromDomain(int $id, string $domain): array {
|
||||
$json_base_url = trim((string) (
|
||||
$this->config('dfg_3dviewer.settings')->get('dfg_3dviewer_json_export_base_url')
|
||||
?? $this->config('dfg_3dviewer.settings')->get('json_export_base_url')
|
||||
?? ''
|
||||
));
|
||||
$source = trim($json_base_url !== '' ? $json_base_url : $domain);
|
||||
$url = $this->resolveJsonRecordUrl($source, $id);
|
||||
if ($url === '') {
|
||||
throw new \RuntimeException('Missing base URL for JSON source fetch');
|
||||
}
|
||||
|
||||
$response = $this->httpClient->request('GET', $url, ['http_errors' => false]);
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new \RuntimeException('JSON source fetch failed with status ' . $response->getStatusCode());
|
||||
}
|
||||
|
||||
$payload = json_decode((string) $response->getBody(), true);
|
||||
if (!is_array($payload)) {
|
||||
throw new \RuntimeException('JSON source returned invalid payload');
|
||||
}
|
||||
|
||||
$record = $payload[0] ?? $payload;
|
||||
if (!is_array($record) || empty($record)) {
|
||||
throw new \RuntimeException('JSON source returned an empty record');
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
protected function resolveJsonRecordUrl(string $source, int $id): string {
|
||||
$source = trim($source);
|
||||
if ($source === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('#/api/digital_reconstruction/record/\d+/?$#', $source)) {
|
||||
return rtrim($source, '/');
|
||||
}
|
||||
|
||||
if (preg_match('#/api/digital_reconstruction/record/?$#', $source)) {
|
||||
return rtrim($source, '/') . '/' . $id;
|
||||
}
|
||||
|
||||
$base_url = $this->normalizeDomain($source);
|
||||
if ($base_url === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $base_url . sprintf(self::JSON_EXPORT_PATH, $id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform XML using XSLT.
|
||||
*/
|
||||
protected function transformXml(\SimpleXMLElement $xml, string $domain): string {
|
||||
$xsl = simplexml_load_string($this->fetchXsl());
|
||||
if (!$xsl) {
|
||||
throw new \RuntimeException('Cannot load XSL');
|
||||
}
|
||||
|
||||
$xslt = new \XSLTProcessor();
|
||||
$xslt->importStyleSheet($xsl);
|
||||
|
||||
$result = $xslt->transformToXML($xml);
|
||||
if ($result === false) {
|
||||
throw new \RuntimeException('XSLT transformation failed');
|
||||
}
|
||||
|
||||
$result = $this->normalizeDefaultHostUrls($result, $domain);
|
||||
return $this->formatXml($result);
|
||||
}
|
||||
|
||||
protected function buildXmlFromJsonRecord(array $record, int $id, string $domain): string {
|
||||
$domain = $this->normalizeDomain($domain);
|
||||
$title = $this->stringValue($record, 'title', 'Digital reconstruction ' . $id);
|
||||
$converted_file = $this->normalizeArchiveModelPath(
|
||||
$this->extractModelUrlFromRecord($record)
|
||||
);
|
||||
if ($converted_file === '') {
|
||||
throw new \RuntimeException(
|
||||
'JSON record does not contain a 3D file URL. Available keys: ' . implode(', ', array_keys($record))
|
||||
);
|
||||
}
|
||||
|
||||
$preview = $this->firstNonEmptyValue($record, ['object_preview', 'preview', 'reconstruction_previews']);
|
||||
$metadata_export = $this->firstNonEmptyValue($record, ['metadata_export']);
|
||||
$viewer_metadata = $this->loadViewerMetadataFromModelUrl($converted_file);
|
||||
$iiif_annotations_xml = trim((string) (
|
||||
$viewer_metadata['iiifAnnotationsXml']
|
||||
?? $viewer_metadata['iiif_annotations_xml']
|
||||
?? $viewer_metadata['annotationsXml']
|
||||
?? $viewer_metadata['annotations_xml']
|
||||
?? ''
|
||||
));
|
||||
$annotation_entries = $this->extractAnnotationEntriesFromViewerMetadata($viewer_metadata);
|
||||
$object_uri = $this->firstNonEmptyValue($record, ['object_URI', 'URI']);
|
||||
$description = $this->firstNonEmptyValue($record, ['object_description']);
|
||||
$authors = $this->firstNonEmptyValue($record, ['reconstruction_authors']);
|
||||
$authors_affiliation = $this->firstNonEmptyValue($record, ['reconstruction_authors_affiliation']);
|
||||
$license = $this->firstNonEmptyValue($record, ['reconstruction_license']);
|
||||
$time_frame = $this->firstNonEmptyValue($record, ['reconstruction_time_frame']);
|
||||
$edition_date = $this->firstNonEmptyValue($record, ['edition_date']);
|
||||
$object_name = $this->firstNonEmptyValue($record, ['object_name']);
|
||||
$object_type = $this->firstNonEmptyValue($record, ['object_type']);
|
||||
$object_category = $this->firstNonEmptyValue($record, ['object_category']);
|
||||
$project_name = $this->firstNonEmptyValue($record, ['project_name']);
|
||||
$project_acronym = $this->firstNonEmptyValue($record, ['project_acronym']);
|
||||
$custody = $this->firstNonEmptyValue($record, ['reconstruction_custody']);
|
||||
$status = $this->firstNonEmptyValue($record, ['status']);
|
||||
|
||||
$dom = new \DOMDocument('1.0', 'UTF-8');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
|
||||
$mets = $dom->createElementNS('http://www.loc.gov/METS/', 'mets:mets');
|
||||
$mets->setAttribute('OBJID', (string) $id);
|
||||
$mets->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:mods', 'http://www.loc.gov/mods/v3');
|
||||
$mets->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
||||
$mets->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:iiif', 'http://iiif.io/api/presentation/3#');
|
||||
$dom->appendChild($mets);
|
||||
|
||||
$metsHdr = $dom->createElement('mets:metsHdr');
|
||||
if ($edition_date !== '') {
|
||||
$metsHdr->setAttribute('LASTMODDATE', $edition_date);
|
||||
}
|
||||
$mets->appendChild($metsHdr);
|
||||
|
||||
$dmdSec = $dom->createElement('mets:dmdSec');
|
||||
$dmdSec->setAttribute('ID', 'DMD1');
|
||||
$mets->appendChild($dmdSec);
|
||||
|
||||
$mdWrap = $dom->createElement('mets:mdWrap');
|
||||
$mdWrap->setAttribute('MDTYPE', 'MODS');
|
||||
$dmdSec->appendChild($mdWrap);
|
||||
|
||||
$xmlData = $dom->createElement('mets:xmlData');
|
||||
$mdWrap->appendChild($xmlData);
|
||||
|
||||
$mods = $dom->createElement('mods:mods');
|
||||
$xmlData->appendChild($mods);
|
||||
|
||||
$titleInfo = $dom->createElement('mods:titleInfo');
|
||||
$mods->appendChild($titleInfo);
|
||||
$titleInfo->appendChild($dom->createElement('mods:title', $title));
|
||||
|
||||
foreach ([
|
||||
'reconstruction_authors' => $authors,
|
||||
'reconstruction_authors_affiliation' => $authors_affiliation,
|
||||
'object_name' => $object_name,
|
||||
'object_type' => $object_type,
|
||||
'object_category' => $object_category,
|
||||
'project_name' => $project_name,
|
||||
'project_acronym' => $project_acronym,
|
||||
'reconstruction_custody' => $custody,
|
||||
'status' => $status,
|
||||
'reconstruction_time_frame' => $time_frame,
|
||||
] as $label => $value) {
|
||||
if ($value === '') {
|
||||
continue;
|
||||
}
|
||||
$note = $dom->createElement('mods:note', $value);
|
||||
$note->setAttribute('type', $label);
|
||||
$mods->appendChild($note);
|
||||
}
|
||||
|
||||
if ($authors !== '') {
|
||||
$name = $dom->createElement('mods:name');
|
||||
$name->setAttribute('type', 'personal');
|
||||
$mods->appendChild($name);
|
||||
$name->appendChild($dom->createElement('mods:namePart', $authors));
|
||||
$role = $dom->createElement('mods:role');
|
||||
$name->appendChild($role);
|
||||
$role->appendChild($dom->createElement('mods:roleTerm', 'creator'));
|
||||
}
|
||||
|
||||
if ($description !== '') {
|
||||
$mods->appendChild($dom->createElement('mods:abstract', $description));
|
||||
}
|
||||
|
||||
if ($license !== '') {
|
||||
$mods->appendChild($dom->createElement('mods:accessCondition', $license));
|
||||
}
|
||||
|
||||
if ($edition_date !== '') {
|
||||
$originInfo = $dom->createElement('mods:originInfo');
|
||||
$originInfo->appendChild($dom->createElement('mods:dateIssued', $edition_date));
|
||||
$mods->appendChild($originInfo);
|
||||
}
|
||||
|
||||
if ($object_uri !== '' || $metadata_export !== '' || $domain !== '') {
|
||||
$location = $dom->createElement('mods:location');
|
||||
$mods->appendChild($location);
|
||||
if ($object_uri !== '') {
|
||||
$location->appendChild($dom->createElement('mods:url', $object_uri));
|
||||
}
|
||||
if ($metadata_export !== '') {
|
||||
$url = $dom->createElement('mods:url', $metadata_export);
|
||||
$url->setAttribute('usage', 'primary display');
|
||||
$location->appendChild($url);
|
||||
}
|
||||
if ($domain !== '') {
|
||||
$location->appendChild($dom->createElement('mods:physicalLocation', $domain));
|
||||
}
|
||||
}
|
||||
|
||||
$fileSec = $dom->createElement('mets:fileSec');
|
||||
$mets->appendChild($fileSec);
|
||||
|
||||
$modelGroup = $dom->createElement('mets:fileGrp');
|
||||
$modelGroup->setAttribute('USE', 'MODEL');
|
||||
$fileSec->appendChild($modelGroup);
|
||||
|
||||
$modelFile = $dom->createElement('mets:file');
|
||||
$modelFile->setAttribute('ID', 'FILE_MODEL');
|
||||
$modelFile->setAttribute('MIMETYPE', $this->guessMimeTypeFromUrl($converted_file));
|
||||
$modelGroup->appendChild($modelFile);
|
||||
|
||||
$modelFLocat = $dom->createElement('mets:FLocat');
|
||||
$modelFLocat->setAttribute('LOCTYPE', 'URL');
|
||||
$modelFLocat->setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', $converted_file);
|
||||
$modelFile->appendChild($modelFLocat);
|
||||
|
||||
if ($preview !== '') {
|
||||
$thumbGroup = $dom->createElement('mets:fileGrp');
|
||||
$thumbGroup->setAttribute('USE', 'THUMBNAIL');
|
||||
$fileSec->appendChild($thumbGroup);
|
||||
|
||||
$thumbFile = $dom->createElement('mets:file');
|
||||
$thumbFile->setAttribute('ID', 'FILE_PREVIEW');
|
||||
$thumbFile->setAttribute('MIMETYPE', $this->guessMimeTypeFromUrl($preview));
|
||||
$thumbGroup->appendChild($thumbFile);
|
||||
|
||||
$thumbFLocat = $dom->createElement('mets:FLocat');
|
||||
$thumbFLocat->setAttribute('LOCTYPE', 'URL');
|
||||
$thumbFLocat->setAttributeNS('http://www.w3.org/1999/xlink', 'xlink:href', $preview);
|
||||
$thumbFile->appendChild($thumbFLocat);
|
||||
}
|
||||
|
||||
$structMap = $dom->createElement('mets:structMap');
|
||||
$structMap->setAttribute('TYPE', 'LOGICAL');
|
||||
$mets->appendChild($structMap);
|
||||
|
||||
$div = $dom->createElement('mets:div');
|
||||
$div->setAttribute('TYPE', 'monograph');
|
||||
$div->setAttribute('DMDID', 'DMD1');
|
||||
$div->setAttribute('LABEL', $title);
|
||||
$structMap->appendChild($div);
|
||||
|
||||
$fptr = $dom->createElement('mets:fptr');
|
||||
$fptr->setAttribute('FILEID', 'FILE_MODEL');
|
||||
$div->appendChild($fptr);
|
||||
|
||||
$amdSec = $dom->createElement('mets:amdSec');
|
||||
$mets->appendChild($amdSec);
|
||||
$techMD = $dom->createElement('mets:techMD');
|
||||
$techMD->setAttribute('ID', 'TECH1');
|
||||
$amdSec->appendChild($techMD);
|
||||
$techWrap = $dom->createElement('mets:mdWrap');
|
||||
$techWrap->setAttribute('MDTYPE', 'OTHER');
|
||||
$techWrap->setAttribute('OTHERMDTYPE', 'DFG3D');
|
||||
$techMD->appendChild($techWrap);
|
||||
$techXmlData = $dom->createElement('mets:xmlData');
|
||||
$techWrap->appendChild($techXmlData);
|
||||
$techXmlData->appendChild($dom->createElement('converted_file', $converted_file));
|
||||
if ($preview !== '') {
|
||||
$techXmlData->appendChild($dom->createElement('object_preview', $preview));
|
||||
}
|
||||
if ($metadata_export !== '') {
|
||||
$techXmlData->appendChild($dom->createElement('metadata_export', $metadata_export));
|
||||
}
|
||||
if ($iiif_annotations_xml !== '') {
|
||||
$rawNode = $dom->createElement('iiif_annotations_xml');
|
||||
$rawNode->appendChild($dom->createCDATASection($iiif_annotations_xml));
|
||||
$techXmlData->appendChild($rawNode);
|
||||
}
|
||||
$iiifNode = $this->buildIiifAnnotationsNode($dom, $iiif_annotations_xml, $annotation_entries);
|
||||
if ($iiifNode instanceof \DOMNode) {
|
||||
$techXmlData->appendChild($iiifNode);
|
||||
}
|
||||
|
||||
$xml = $dom->saveXML();
|
||||
if ($xml === false) {
|
||||
throw new \RuntimeException('Cannot build XML from JSON record.');
|
||||
}
|
||||
|
||||
$xml = $this->normalizeDefaultHostUrls($xml, $domain);
|
||||
return $this->formatXml($xml);
|
||||
}
|
||||
|
||||
protected function enrichJsonRecordFromLocalEntity(array $record, int $id): array {
|
||||
$current_model = $this->extractModelUrlFromRecord($record);
|
||||
if ($current_model !== '') {
|
||||
if ($this->stringValue($record, '3D_file') === '') {
|
||||
$record['3D_file'] = $current_model;
|
||||
$source = $this->detectModelSourceInRecord($record);
|
||||
\Drupal::logger('dfg_3dviewer')->notice(
|
||||
'Filled missing JSON 3D_file for entity @id from existing record source "@source": @value',
|
||||
[
|
||||
'@id' => (string) $id,
|
||||
'@source' => $source,
|
||||
'@value' => $current_model,
|
||||
]
|
||||
);
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
$cfg = $this->config('dfg_3dviewer.settings');
|
||||
$field_candidates = array_values(array_filter(array_unique(array_merge([
|
||||
trim((string) ($cfg->get('dfg_3dviewer_viewer_file_name') ?? $cfg->get('viewer_file_name') ?? '')),
|
||||
trim((string) ($cfg->get('dfg_3dviewer_viewer_file_upload') ?? $cfg->get('viewer_file_upload') ?? '')),
|
||||
trim((string) ($cfg->get('dfg_3dviewer_api_3d_file_field') ?? $cfg->get('api_3d_file_field') ?? '')),
|
||||
], self::ADDITIONAL_MODEL_FIELD_CANDIDATES))));
|
||||
|
||||
foreach (['wisski_individual', 'node'] as $entity_type) {
|
||||
try {
|
||||
$entity = \Drupal::entityTypeManager()->getStorage($entity_type)->load($id);
|
||||
if (!$entity) {
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach ($field_candidates as $field_name) {
|
||||
$resolved = $this->resolveEntityFieldToPublicUrl($entity, $field_name);
|
||||
if ($resolved === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$record['3D_file'] = $resolved;
|
||||
\Drupal::logger('dfg_3dviewer')->notice(
|
||||
'Filled missing JSON 3D_file for entity @id from local field "@field": @value',
|
||||
[
|
||||
'@id' => (string) $id,
|
||||
'@field' => $field_name,
|
||||
'@value' => $resolved,
|
||||
]
|
||||
);
|
||||
return $record;
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
// Try the next entity type.
|
||||
}
|
||||
}
|
||||
|
||||
return $record;
|
||||
}
|
||||
|
||||
protected function resolveEntityFieldToPublicUrl($entity, string $field_name): string {
|
||||
if ($field_name === '' || !method_exists($entity, 'hasField') || !$entity->hasField($field_name)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$values = $entity->get($field_name)->getValue();
|
||||
$first = is_array($values[0] ?? null) ? $values[0] : [];
|
||||
if (empty($first)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!empty($first['target_id']) && ctype_digit((string) $first['target_id'])) {
|
||||
$file = \Drupal\file\Entity\File::load((int) $first['target_id']);
|
||||
if ($file) {
|
||||
return $this->fileUriToPublicUrl((string) $file->getFileUri());
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['value', 'uri'] as $key) {
|
||||
$candidate = trim((string) ($first[$key] ?? ''));
|
||||
if ($candidate === '') {
|
||||
continue;
|
||||
}
|
||||
if (preg_match('#^https?://#i', $candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $candidate)) {
|
||||
return $this->fileUriToPublicUrl($candidate);
|
||||
}
|
||||
if (str_starts_with($candidate, '/sites/default/files/')) {
|
||||
$base = $this->preferredPublicBaseUrl();
|
||||
return $base !== '' ? rtrim($base, '/') . $candidate : $candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function fileUriToPublicUrl(string $uri): string {
|
||||
$uri = trim($uri);
|
||||
if ($uri === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (preg_match('#^https?://#i', $uri)) {
|
||||
return $uri;
|
||||
}
|
||||
|
||||
if (str_starts_with($uri, 'public://')) {
|
||||
$relative = '/sites/default/files/' . ltrim(substr($uri, strlen('public://')), '/');
|
||||
$base = $this->preferredPublicBaseUrl();
|
||||
return $base !== '' ? rtrim($base, '/') . $relative : $relative;
|
||||
}
|
||||
|
||||
try {
|
||||
$generated = (string) \Drupal::service('file_url_generator')->generateAbsoluteString($uri);
|
||||
$host = (string) parse_url($generated, PHP_URL_HOST);
|
||||
$path = (string) parse_url($generated, PHP_URL_PATH);
|
||||
if ($host !== '' && (strpos($host, '_') !== false || strtolower($host) === 'default') && $path !== '') {
|
||||
$base = $this->preferredPublicBaseUrl();
|
||||
return $base !== '' ? rtrim($base, '/') . $path : $path;
|
||||
}
|
||||
return $generated;
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
protected function preferredPublicBaseUrl(): string {
|
||||
$cfg = $this->config('dfg_3dviewer.settings');
|
||||
$candidates = [
|
||||
trim((string) ($cfg->get('dfg_3dviewer_main_url') ?? $cfg->get('main_url') ?? '')),
|
||||
trim((string) ($cfg->get('dfg_3dviewer_json_export_base_url') ?? $cfg->get('json_export_base_url') ?? '')),
|
||||
];
|
||||
|
||||
foreach ($candidates as $candidate) {
|
||||
$parts = parse_url($candidate);
|
||||
$host = is_array($parts) ? (string) ($parts['host'] ?? '') : '';
|
||||
if (is_array($parts) && !empty($parts['scheme']) && $host !== '' && strpos($host, '_') === false && strtolower($host) !== 'default') {
|
||||
return rtrim($candidate, '/');
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function firstNonEmptyValue(array $record, array $keys): string {
|
||||
foreach ($keys as $key) {
|
||||
$value = $this->stringValue($record, $key);
|
||||
if ($value !== '') {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function stringValue(array $record, string $key, string $default = ''): string {
|
||||
if (!array_key_exists($key, $record)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$value = $record[$key];
|
||||
if (is_array($value)) {
|
||||
$value = implode(', ', array_filter(array_map('strval', $value)));
|
||||
}
|
||||
|
||||
return trim((string) $value) ?: $default;
|
||||
}
|
||||
|
||||
protected function guessMimeTypeFromUrl(string $url): string {
|
||||
$path = parse_url($url, PHP_URL_PATH);
|
||||
$extension = strtolower(pathinfo((string) $path, PATHINFO_EXTENSION));
|
||||
|
||||
return match ($extension) {
|
||||
'glb' => 'model/gltf-binary',
|
||||
'gltf' => 'model/gltf+json',
|
||||
'fbx' => 'application/octet-stream',
|
||||
'obj' => 'text/plain',
|
||||
'stl' => 'model/stl',
|
||||
'ply' => 'application/octet-stream',
|
||||
'dae' => 'model/vnd.collada+xml',
|
||||
'jpg', 'jpeg' => 'image/jpeg',
|
||||
'png' => 'image/png',
|
||||
'webp' => 'image/webp',
|
||||
default => 'application/octet-stream',
|
||||
};
|
||||
}
|
||||
|
||||
protected function extractModelUrlFromRecord(array $record): string {
|
||||
$candidate = $this->firstNonEmptyValue($record, [
|
||||
'converted_file',
|
||||
'3D_file',
|
||||
'3d_file_original',
|
||||
'3D_file_original',
|
||||
'3d_file',
|
||||
'model_file',
|
||||
'model',
|
||||
'file',
|
||||
'viewer_file',
|
||||
'viewer_file_name',
|
||||
]);
|
||||
if ($this->isModelUrl($candidate)) {
|
||||
return $candidate;
|
||||
}
|
||||
|
||||
foreach ($this->flattenRecordStrings($record) as $value) {
|
||||
if ($this->isModelUrl($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
protected function detectModelSourceInRecord(array $record): string {
|
||||
$ordered_keys = [
|
||||
'converted_file',
|
||||
'3D_file',
|
||||
'3d_file_original',
|
||||
'3D_file_original',
|
||||
'3d_file',
|
||||
'model_file',
|
||||
'model',
|
||||
'file',
|
||||
'viewer_file',
|
||||
'viewer_file_name',
|
||||
];
|
||||
|
||||
foreach ($ordered_keys as $key) {
|
||||
$value = $this->stringValue($record, $key);
|
||||
if ($value !== '' && $this->isModelUrl($value)) {
|
||||
return $key;
|
||||
}
|
||||
}
|
||||
|
||||
return 'flattened_record_scan';
|
||||
}
|
||||
|
||||
protected function flattenRecordStrings(array $record): array {
|
||||
$values = [];
|
||||
|
||||
foreach ($record as $value) {
|
||||
if (is_array($value)) {
|
||||
$values = array_merge($values, $this->flattenRecordStrings($value));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (is_scalar($value) || (is_object($value) && method_exists($value, '__toString'))) {
|
||||
$values[] = trim((string) $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $values;
|
||||
}
|
||||
|
||||
protected function isModelUrl(string $value): bool {
|
||||
if ($value === '' || !preg_match('#^https?://#i', $value)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$path = (string) parse_url($value, PHP_URL_PATH);
|
||||
$extension = strtolower(pathinfo($path, PATHINFO_EXTENSION));
|
||||
|
||||
return in_array($extension, ['glb', 'gltf', 'fbx', 'obj', 'stl', 'ply', 'dae', '3ds', 'ifc', 'xyz', 'pcd', 'abc'], true);
|
||||
}
|
||||
|
||||
protected function loadViewerMetadataFromModelUrl(string $model_url): array {
|
||||
$path = (string) parse_url($model_url, PHP_URL_PATH);
|
||||
if ($path === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$prefix = '/sites/default/files/';
|
||||
$prefix_pos = strpos($path, $prefix);
|
||||
if ($prefix_pos === false) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$relative = ltrim(substr($path, $prefix_pos + strlen($prefix)), '/');
|
||||
if ($relative === '') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$relative = urldecode($relative);
|
||||
$dirname = trim(dirname($relative), '/');
|
||||
$basename = pathinfo($relative, PATHINFO_FILENAME);
|
||||
if ($basename === '' || $basename === '.') {
|
||||
return [];
|
||||
}
|
||||
|
||||
$viewer_uri = 'public://' . ($dirname !== '' ? $dirname . '/' : '') . 'metadata/' . $basename . '_viewer.json';
|
||||
$real_path = $this->fileSystem->realpath($viewer_uri);
|
||||
if ($real_path === false || !is_file($real_path)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
try {
|
||||
$content = file_get_contents($real_path);
|
||||
if ($content === false || trim($content) === '') {
|
||||
return [];
|
||||
}
|
||||
$decoded = json_decode($content, true);
|
||||
return is_array($decoded) ? $decoded : [];
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
protected function extractAnnotationEntriesFromViewerMetadata(array $viewer_metadata): array {
|
||||
$entries = $viewer_metadata['annotationEntries'] ?? [];
|
||||
if (!is_array($entries)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($entries as $entry) {
|
||||
if (!is_array($entry)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$target_id = trim((string) (
|
||||
$entry['targetId']
|
||||
?? $entry['object']
|
||||
?? ($entry['target']['id'] ?? '')
|
||||
));
|
||||
if ($target_id === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$faces = [];
|
||||
$raw_faces = $entry['faceNumbers']
|
||||
?? ($entry['target']['faces'] ?? [$entry['faceIndex'] ?? null]);
|
||||
if (is_array($raw_faces)) {
|
||||
foreach ($raw_faces as $face) {
|
||||
if (is_numeric($face) && (int) $face >= 0) {
|
||||
$faces[] = (int) $face;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (empty($faces)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$normalized[] = [
|
||||
'id' => trim((string) ($entry['id'] ?? '')),
|
||||
'targetId' => $target_id,
|
||||
'faces' => array_values(array_unique($faces)),
|
||||
'title' => trim((string) ($entry['title'] ?? '')),
|
||||
'description' => trim((string) ($entry['description'] ?? '')),
|
||||
];
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
protected function buildIiifAnnotationsNode(
|
||||
\DOMDocument $dom,
|
||||
string $iiif_annotations_xml,
|
||||
array $annotation_entries
|
||||
): ?\DOMNode {
|
||||
$iiif_annotations_xml = trim($iiif_annotations_xml);
|
||||
if ($iiif_annotations_xml !== '') {
|
||||
try {
|
||||
$tmp = new \DOMDocument('1.0', 'UTF-8');
|
||||
$tmp->loadXML($iiif_annotations_xml);
|
||||
if ($tmp->documentElement instanceof \DOMElement) {
|
||||
return $dom->importNode($tmp->documentElement, true);
|
||||
}
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
// Fallback to normalized entry export below.
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($annotation_entries)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$root = $dom->createElement('iiif:annotations');
|
||||
$root->setAttribute('version', '3.0');
|
||||
|
||||
foreach ($annotation_entries as $entry) {
|
||||
$target_id = trim((string) ($entry['targetId'] ?? ''));
|
||||
$faces = is_array($entry['faces'] ?? null) ? $entry['faces'] : [];
|
||||
if ($target_id === '' || empty($faces)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$annotation = $dom->createElement('iiif:annotation');
|
||||
if (!empty($entry['id'])) {
|
||||
$annotation->setAttribute('id', (string) $entry['id']);
|
||||
}
|
||||
$annotation->setAttribute('type', 'Annotation');
|
||||
$annotation->setAttribute('motivation', 'commenting');
|
||||
|
||||
$body = $dom->createElement('iiif:body');
|
||||
$body->setAttribute('type', 'TextualBody');
|
||||
$body->setAttribute('format', 'text/plain');
|
||||
$body->appendChild($dom->createElement('iiif:title', (string) ($entry['title'] ?? '')));
|
||||
$body->appendChild($dom->createElement('iiif:description', (string) ($entry['description'] ?? '')));
|
||||
$annotation->appendChild($body);
|
||||
|
||||
$target = $dom->createElement('iiif:target');
|
||||
$target->setAttribute('id', $target_id);
|
||||
$target->setAttribute('faces', implode(',', array_map('strval', $faces)));
|
||||
$annotation->appendChild($target);
|
||||
|
||||
$root->appendChild($annotation);
|
||||
}
|
||||
|
||||
if (!$root->hasChildNodes()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
protected function normalizeDefaultHostUrls(string $xml, string $domain): string {
|
||||
$domain = $this->normalizeDomain($domain);
|
||||
if ($domain === '') {
|
||||
return $this->normalizeArchiveModelPath($xml);
|
||||
}
|
||||
|
||||
$normalized = preg_replace('#https?://(default|dfg_3dviewer)(?=/)#i', $domain, $xml);
|
||||
$escaped = preg_quote($domain, '#');
|
||||
|
||||
$normalized = preg_replace(
|
||||
"#{$escaped}/sites/default/files/wisski_original/{$escaped}#i",
|
||||
$domain,
|
||||
$normalized
|
||||
);
|
||||
$normalized = preg_replace(
|
||||
"#https?://[^/]+/sites/default/files/wisski_original/{$escaped}#i",
|
||||
$domain,
|
||||
$normalized
|
||||
);
|
||||
|
||||
return $this->normalizeArchiveModelPath($normalized);
|
||||
}
|
||||
|
||||
protected function normalizeArchiveModelPath(string $value): string {
|
||||
return preg_replace(
|
||||
'#(/[^/"\'<>\s]+_(?:ZIP|RAR|TAR|XZ|GZ))/([^/"\'<>\s]+\.(?:glb|gltf))#i',
|
||||
'$1/gltf/$2',
|
||||
$value
|
||||
) ?? $value;
|
||||
}
|
||||
|
||||
protected function fetchXsl(): string {
|
||||
$response = $this->httpClient->request('GET', self::XSL_URL, ['http_errors' => false]);
|
||||
if ($response->getStatusCode() !== 200) {
|
||||
throw new \RuntimeException('Cannot fetch XSL: ' . $response->getStatusCode());
|
||||
}
|
||||
return (string) $response->getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* Pretty-print XML.
|
||||
*/
|
||||
protected function formatXml(string $xml): string {
|
||||
$dom = new \DOMDocument('1.0');
|
||||
$dom->preserveWhiteSpace = false;
|
||||
$dom->formatOutput = true;
|
||||
$dom->loadXML($xml);
|
||||
|
||||
return $dom->saveXML();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save XML to files directory.
|
||||
*/
|
||||
protected function saveXml(string $id, string $xml): string {
|
||||
$directory = self::FILE_DIR;
|
||||
$this->fileSystem->prepareDirectory(
|
||||
$directory,
|
||||
FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS
|
||||
);
|
||||
|
||||
$path = $directory . '/' . $id . '.xml';
|
||||
$real_path = $this->fileSystem->realpath($directory);
|
||||
|
||||
file_put_contents($real_path . '/' . $id . '.xml', $xml);
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
}
|
||||
360
src/Form/DFG3dViewerConfigForm.php
Normal file
360
src/Form/DFG3dViewerConfigForm.php
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Form;
|
||||
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
class DFG3dViewerConfigForm extends FormBase {
|
||||
|
||||
/**
|
||||
* Normalizes the viewer module path to a path-first value.
|
||||
*/
|
||||
protected function normalizeBaseModulePath(string $value): string {
|
||||
$value = trim($value);
|
||||
|
||||
if ($value === '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (preg_match('@^https?://@i', $value)) {
|
||||
$parts = parse_url($value);
|
||||
if (!empty($parts['path'])) {
|
||||
$value = $parts['path'];
|
||||
}
|
||||
}
|
||||
elseif (preg_match('@^/[^/]+\.[^/]+/.+@', $value)) {
|
||||
$segments = array_values(array_filter(explode('/', $value), 'strlen'));
|
||||
array_shift($segments);
|
||||
$value = '/' . implode('/', $segments);
|
||||
}
|
||||
elseif (preg_match('@^[^/]+\.[^/]+/.+@', $value)) {
|
||||
$segments = explode('/', $value);
|
||||
array_shift($segments);
|
||||
$value = '/' . implode('/', $segments);
|
||||
}
|
||||
|
||||
$value = preg_replace('@/{2,}@', '/', $value);
|
||||
|
||||
if ($value !== '' && $value[0] !== '/') {
|
||||
$value = '/' . $value;
|
||||
}
|
||||
|
||||
return rtrim($value, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function getFormId() {
|
||||
|
||||
return 'dfg_3dviewer_settings_form';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
|
||||
$settings = $this->configFactory()->getEditable('dfg_3dviewer.settings');
|
||||
$default_config = \Drupal::config('dfg_3dviewer.settings');
|
||||
$default_settings = [
|
||||
'entity_bundle' => $default_config->get('dfg_3dviewer_entitybundle'),
|
||||
'viewer_file_upload' => $default_config->get('dfg_3dviewer_viewer_file_upload'),
|
||||
'image_generation' => $default_config->get('dfg_3dviewer_image_generation'),
|
||||
'viewer_file_name' => $default_config->get('dfg_3dviewer_viewer_file_name'),
|
||||
'api_3d_file_field' => $default_config->get('dfg_3dviewer_api_3d_file_field') ?? $default_config->get('api_3d_file_field'),
|
||||
'field_df' => $default_config->get('dfg_3dviewer_field_df'),
|
||||
'main_url' => $default_config->get('dfg_3dviewer_main_url') ?? $default_config->get('main_url'),
|
||||
'metadata_url' => $default_config->get('dfg_3dviewer_metadata_url') ?? $default_config->get('metadata_url'),
|
||||
'json_export_base_url' => $default_config->get('dfg_3dviewer_json_export_base_url') ?? $default_config->get('json_export_base_url'),
|
||||
'basenamespace' => $default_config->get('dfg_3dviewer_basenamespace'),
|
||||
'container' => $default_config->get('dfg_3dviewer_container'),
|
||||
'lightweight' => $default_config->get('dfg_3dviewer_lightweight'),
|
||||
'scale_container_x' => $default_config->get('dfg_3dviewer_scale_container_x'),
|
||||
'scale_container_y' => $default_config->get('dfg_3dviewer_scale_container_y'),
|
||||
'gallery_container' => $default_config->get('dfg_3dviewer_gallery_container'),
|
||||
'gallery_image_class' => $default_config->get('dfg_3dviewer_gallery_image_class'),
|
||||
'gallery_image_id' => $default_config->get('dfg_3dviewer_gallery_image_id'),
|
||||
'base_module_path' => $default_config->get('dfg_3dviewer_base_module_path') ?: '/libraries/dfg-3dviewer/assets',
|
||||
'entity_id_uri' => $default_config->get('dfg_3dviewer_entity_id_uri'),
|
||||
'view_entity_path' => $default_config->get('dfg_3dviewer_view_entity_path'),
|
||||
'attribute_id' => $default_config->get('dfg_3dviewer_attribute_id'),
|
||||
'export_viewer' => $default_config->get('dfg_3dviewer_export_viewer'),
|
||||
'export_viewer_url' => $default_config->get('dfg_3dviewer_export_viewer_url'),
|
||||
];
|
||||
|
||||
$form['#dfg_3dviewer_settings'] = $settings;
|
||||
|
||||
$form['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||
dfg_3dviewer_attach_settings($form);
|
||||
|
||||
$form['dfg_3dviewer_main_url'] = [
|
||||
'#default_value' => $default_settings['main_url'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Main URL'),
|
||||
'#required' => true,
|
||||
'#description' => 'Change <b>main URL</b> for used repository',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_basenamespace'] = [
|
||||
'#default_value' => $default_settings['basenamespace'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Default base namespace'),
|
||||
'#description' => $this->t('(if different than Main URL)'),
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_metadata_url'] = [
|
||||
'#default_value' => $default_settings['metadata_url'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Metadata URL'),
|
||||
'#description' => '<b>URL</b> of the instance that serves metadata content'
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_json_export_base_url'] = [
|
||||
'#default_value' => $default_settings['json_export_base_url'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('JSON Export Base URL'),
|
||||
'#description' => '<b>Base URL</b> of the instance that serves JSON export, e.g. https://repository.covher.eu'
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_container'] = [
|
||||
'#default_value' => $default_settings['container'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Container ID'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>ID</b> of the main container for the Viewer',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_entitybundle'] = [
|
||||
'#default_value' => $default_settings['entity_bundle'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Entity Bundle ID'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for 3d_model field',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_viewer_file_upload'] = [
|
||||
'#default_value' => $default_settings['viewer_file_upload'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Viewer File Upload ID'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for 3d_upload field',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_viewer_file_name'] = [
|
||||
'#default_value' => $default_settings['viewer_file_name'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Viewer File Name ID'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for viewer_file_name field',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_api_3d_file_field'] = [
|
||||
'#default_value' => $default_settings['api_3d_file_field'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('API 3D File Field'),
|
||||
'#required' => false,
|
||||
'#description' => '<b>ID or machine name</b> of the field that should populate API `3D_file`'
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_image_generation'] = [
|
||||
'#default_value' => $default_settings['image_generation'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Image Generation'),
|
||||
'#description' => '<b>ID</b> of the bundle for the entity given in wisski pathbuilder for image_generation field'
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_field_df'] = [
|
||||
'#default_value' => $default_settings['field_df'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Field DF'),
|
||||
'#description' => 'Name of the field given for <b>field_df</b>'
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_export_viewer'] = [
|
||||
'#default_value' => $default_settings['export_viewer'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Export Viewer Field'),
|
||||
'#description' => 'Name of the field given for <b>export_viewer</b>',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_export_viewer_url'] = [
|
||||
'#default_value' => $default_settings['export_viewer_url'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Export Viewer URL'),
|
||||
'#description' => 'URL for the export viewer',
|
||||
];
|
||||
|
||||
$form['scale_wrapper'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['scale-fields-wrapper', 'flex-container'],
|
||||
],
|
||||
];
|
||||
|
||||
$form['scale_wrapper']['dfg_3dviewer_scale_container_x'] = [
|
||||
'#default_value' => $default_settings['scale_container_x'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Scale container X'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>Width</b> scale of the container',
|
||||
'#attributes' => [
|
||||
'class' => ['half-width'],
|
||||
]
|
||||
];
|
||||
|
||||
$form['scale_wrapper']['dfg_3dviewer_scale_container_y'] = [
|
||||
'#default_value' => $default_settings['scale_container_y'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Scale container Y'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>Height</b> scale of the container',
|
||||
'#attributes' => [
|
||||
'class' => ['half-width'],
|
||||
],
|
||||
];
|
||||
|
||||
$form['gallery_wrapper'] = [
|
||||
'#type' => 'container',
|
||||
'#attributes' => [
|
||||
'class' => ['gallery-fields-wrapper', 'gallery-container'],
|
||||
]
|
||||
];
|
||||
|
||||
$form['gallery_wrapper']['dfg_3dviewer_gallery_container'] = [
|
||||
'#default_value' => $default_settings['gallery_container'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Gallery container element name'),
|
||||
'#required' => false,
|
||||
'#description' => '<b>Name</b> of the element with gallery URLs',
|
||||
];
|
||||
|
||||
$form['gallery_wrapper']['dfg_3dviewer_gallery_image_class'] = [
|
||||
'#default_value' => $default_settings['gallery_image_class'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Gallery class name for images'),
|
||||
'#required' => false,
|
||||
'#description' => '<b>Class</b> name for gallery images',
|
||||
];
|
||||
|
||||
$form['gallery_wrapper']['dfg_3dviewer_gallery_image_id'] = [
|
||||
'#default_value' => $default_settings['gallery_image_id'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Gallery ID name for images'),
|
||||
'#required' => false,
|
||||
'#description' => '<b>ID</b> name for gallery images',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_base_module_path'] = [
|
||||
'#default_value' => $default_settings['base_module_path'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Path for the Viewer module'),
|
||||
'#required' => true,
|
||||
'#description' => 'Real <b>path</b> for the Viewer module',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_entity_id_uri'] = [
|
||||
'#default_value' => $default_settings['entity_id_uri'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Regex for entity ID'),
|
||||
'#required' => false,
|
||||
'#description' => '<b>Regex</b> that allows get ID of the entity',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_view_entity_path'] = [
|
||||
'#default_value' => $default_settings['view_entity_path'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Path with navigate content'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>Path</b> that allows navigate to the entity',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_attribute_id'] = [
|
||||
'#default_value' => $default_settings['attribute_id'],
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Attribute ID with WissKI content'),
|
||||
'#required' => true,
|
||||
'#description' => '<b>ID</b> that allows get more specific data from WissKI',
|
||||
];
|
||||
|
||||
$form['dfg_3dviewer_lightweight'] = [
|
||||
'#default_value' => $default_settings['lightweight'],
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('<b>Lightweight</b> version. If checked, 3D Viewer will provide only basic operations.'),
|
||||
'#required' => false
|
||||
];
|
||||
|
||||
$form['submit'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Submit'),
|
||||
];
|
||||
return $form;
|
||||
}
|
||||
|
||||
public function validateForm(array &$form, FormStateInterface $form_state) {
|
||||
if ($form_state->getValue('dfg_3dviewer_lightweight')) {
|
||||
$optional_fields = [
|
||||
'dfg_3dviewer_metadata_url',
|
||||
'dfg_3dviewer_json_export_base_url',
|
||||
'dfg_3dviewer_api_3d_file_field',
|
||||
'dfg_3dviewer_image_generation',
|
||||
'dfg_3dviewer_field_df',
|
||||
'dfg_3dviewer_export_viewer',
|
||||
'dfg_3dviewer_export_viewer_url',
|
||||
'dfg_3dviewer_gallery_container',
|
||||
'dfg_3dviewer_gallery_image_class',
|
||||
'dfg_3dviewer_gallery_image_id',
|
||||
];
|
||||
|
||||
foreach ($optional_fields as $field) {
|
||||
$form_state->setValue($field, '');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
|
||||
$settings = $form['#dfg_3dviewer_settings'];
|
||||
$new_vals = $form_state->getValues();
|
||||
$normalized_base_module_path = $this->normalizeBaseModulePath((string) $new_vals['dfg_3dviewer_base_module_path']);
|
||||
|
||||
$settings->set('dfg_3dviewer_basenamespace', $new_vals['dfg_3dviewer_basenamespace']);
|
||||
$settings->set('dfg_3dviewer_main_url', $new_vals['dfg_3dviewer_main_url']);
|
||||
$settings->set('dfg_3dviewer_metadata_url', $new_vals['dfg_3dviewer_metadata_url']);
|
||||
$settings->set('dfg_3dviewer_json_export_base_url', $new_vals['dfg_3dviewer_json_export_base_url']);
|
||||
$settings->set('dfg_3dviewer_entitybundle', $new_vals['dfg_3dviewer_entitybundle']);
|
||||
$settings->set('dfg_3dviewer_container', $new_vals['dfg_3dviewer_container']);
|
||||
$settings->set('dfg_3dviewer_viewer_file_upload', $new_vals['dfg_3dviewer_viewer_file_upload']);
|
||||
$settings->set('dfg_3dviewer_viewer_file_name', $new_vals['dfg_3dviewer_viewer_file_name']);
|
||||
$settings->set('dfg_3dviewer_api_3d_file_field', $new_vals['dfg_3dviewer_api_3d_file_field']);
|
||||
$settings->set('dfg_3dviewer_image_generation', $new_vals['dfg_3dviewer_image_generation']);
|
||||
$settings->set('dfg_3dviewer_field_df', $new_vals['dfg_3dviewer_field_df']);
|
||||
$settings->set('dfg_3dviewer_lightweight', $new_vals['dfg_3dviewer_lightweight']);
|
||||
$settings->set('dfg_3dviewer_scale_container_x', $new_vals['dfg_3dviewer_scale_container_x']);
|
||||
$settings->set('dfg_3dviewer_scale_container_y', $new_vals['dfg_3dviewer_scale_container_y']);
|
||||
$settings->set('dfg_3dviewer_gallery_container', $new_vals['dfg_3dviewer_gallery_container']);
|
||||
$settings->set('dfg_3dviewer_gallery_image_class', $new_vals['dfg_3dviewer_gallery_image_class']);
|
||||
$settings->set('dfg_3dviewer_gallery_image_id', $new_vals['dfg_3dviewer_gallery_image_id']);
|
||||
$settings->set('dfg_3dviewer_base_module_path', $normalized_base_module_path);
|
||||
$settings->set('dfg_3dviewer_entity_id_uri', $new_vals['dfg_3dviewer_entity_id_uri']);
|
||||
$settings->set('dfg_3dviewer_view_entity_path', $new_vals['dfg_3dviewer_view_entity_path']);
|
||||
$settings->set('dfg_3dviewer_attribute_id', $new_vals['dfg_3dviewer_attribute_id']);
|
||||
$settings->set('dfg_3dviewer_export_viewer', $new_vals['dfg_3dviewer_export_viewer']);
|
||||
$settings->set('dfg_3dviewer_export_viewer_url', $new_vals['dfg_3dviewer_export_viewer_url']);
|
||||
|
||||
$settings->save();
|
||||
|
||||
$this->messenger()->addStatus($this->t('Changed DFG 3D Viewer settings successfully'));
|
||||
$form_state->setRedirect('system.admin_config');
|
||||
}
|
||||
|
||||
}
|
||||
197
src/Plugin/Field/FieldFormatter/DFG3DDerivativeLinkFormatter.php
Executable file
197
src/Plugin/Field/FieldFormatter/DFG3DDerivativeLinkFormatter.php
Executable file
|
|
@ -0,0 +1,197 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\dfg_3dviewer\Plugin\field\formatter\DFG3DDerivativeLinkFormatter.
|
||||
*/
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\image\Entity\ImageStyle;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase;
|
||||
use Drupal\colorbox\Plugin\Field\FieldFormatter\ColorboxFormatter;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'wisski_iip_image' formatter.
|
||||
*
|
||||
* @FieldFormatter(
|
||||
* id = "dfg_3dderivativelink",
|
||||
* module = "dfg_3dderivativelink",
|
||||
* label = @Translation("DFG 3D Derivative Link"),
|
||||
* field_types = {
|
||||
* "file"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
# class WisskiIIPImageFormatter extends ImageFormatterBase {
|
||||
class DFG3DDerivativeLinkFormatter extends FileFormatterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
|
||||
|
||||
// $elements = parent::viewElements($items, $langcode);
|
||||
$elements = array();
|
||||
|
||||
$files = $this->getEntitiesToView($items, $langcode);
|
||||
|
||||
$elements['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||
dfg_3dviewer_attach_settings($elements);
|
||||
|
||||
// get the config
|
||||
$cfg = dfg_3dviewer_config();
|
||||
|
||||
foreach ($files as $delta => $file) {
|
||||
|
||||
// get the filename
|
||||
$filename = $file->getFilename();
|
||||
|
||||
// get pathinfo
|
||||
$pathinfo = pathinfo($file->getFilename());
|
||||
|
||||
// pathinfo without the first extension so bla.tar.gz goes to bla.tar
|
||||
$local_filename = $pathinfo['filename'];
|
||||
|
||||
// bla.tar.gz -> gz
|
||||
$extension = $pathinfo['extension'];
|
||||
|
||||
// thats something like public://2022-01/bla.tar.gz
|
||||
$local_fileuri = $file->getFileUri();
|
||||
|
||||
// we do a switch for compressed file formats because they are handled otherwise.
|
||||
if($extension == "tar" || $extension == "zip" || $extension == "gz" || $extension == "xz" || $extension == "rar") {
|
||||
$local_fileuri = str_replace("." . $extension, "_" . strtoupper($extension), $local_fileuri);
|
||||
|
||||
$local_fileuri = $local_fileuri . "/gltf/" . $local_filename . ".glb";
|
||||
|
||||
} if($extension == "glb" || $extension == "gltf") {
|
||||
// do nothing - just party :D
|
||||
|
||||
} else {
|
||||
$local_fileuri = str_replace($filename, "gltf/" . $filename . ".glb", $local_fileuri);
|
||||
}
|
||||
|
||||
$file_link = $this->uriToUrl($local_fileuri, (string) ($cfg['main_url'] ?? ''));
|
||||
|
||||
$url = Url::fromUri($file_link);
|
||||
|
||||
|
||||
|
||||
$elements[$delta] = array(
|
||||
'#type' => 'link',
|
||||
'#title' => $file_link, //$file->getFilename(),
|
||||
'#url' => $url,
|
||||
);
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
return $elements;
|
||||
|
||||
}
|
||||
|
||||
private function uriToUrl(string $uri, string $public_base_url = ''): ?string {
|
||||
if ($uri === '') {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
$public_base_url = trim($public_base_url);
|
||||
$base_parts = parse_url($public_base_url);
|
||||
$base_host = is_array($base_parts) ? (string) ($base_parts['host'] ?? '') : '';
|
||||
$has_safe_base = is_array($base_parts)
|
||||
&& !empty($base_parts['scheme'])
|
||||
&& $base_host !== ''
|
||||
&& strpos($base_host, '_') === FALSE
|
||||
&& strtolower($base_host) !== 'default';
|
||||
|
||||
// Keep storage deterministic in CLI contexts (e.g. drush) where request
|
||||
// host may resolve to container aliases like "dfg_3dviewer".
|
||||
if (str_starts_with($uri, 'public://')) {
|
||||
$relative_public = '/sites/default/files/' . ltrim(substr($uri, strlen('public://')), '/');
|
||||
return $has_safe_base ? rtrim($public_base_url, '/') . $relative_public : $relative_public;
|
||||
}
|
||||
|
||||
$generator = \Drupal::service('file_url_generator');
|
||||
$relative = (string) $generator->generateString($uri);
|
||||
$relative_parts = parse_url($relative);
|
||||
$relative_host = is_array($relative_parts) ? (string) ($relative_parts['host'] ?? '') : '';
|
||||
$relative_path = is_array($relative_parts) ? (string) ($relative_parts['path'] ?? '') : '';
|
||||
$relative_is_absolute = is_array($relative_parts) && !empty($relative_parts['scheme']) && $relative_host !== '';
|
||||
$relative_has_bad_host = $relative_is_absolute && strpos($relative_host, '_') !== FALSE;
|
||||
if (($relative_has_bad_host || strtolower($relative_host) === 'default')
|
||||
&& str_starts_with($relative_path, '/sites/default/files/')) {
|
||||
$relative = $relative_path;
|
||||
}
|
||||
if (!$relative_is_absolute && str_starts_with($relative, 'sites/default/files/')) {
|
||||
$relative = '/' . ltrim($relative, '/');
|
||||
}
|
||||
|
||||
if ($public_base_url !== '' && $has_safe_base) {
|
||||
if ($relative_is_absolute) {
|
||||
if ($relative_path !== '' && str_starts_with($relative_path, '/sites/default/files/')) {
|
||||
return rtrim($public_base_url, '/') . $relative_path;
|
||||
}
|
||||
return $relative;
|
||||
}
|
||||
return rtrim($public_base_url, '/') . '/' . ltrim($relative, '/');
|
||||
}
|
||||
|
||||
$absolute = $generator->generateAbsoluteString($uri);
|
||||
$host = parse_url($absolute, PHP_URL_HOST);
|
||||
if (is_string($host) && (strpos($host, '_') !== FALSE || strtolower($host) === 'default')) {
|
||||
return $relative;
|
||||
}
|
||||
|
||||
return $absolute;
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
\Drupal::logger('dfg_3dviewer')->warning(
|
||||
'Cannot build URL for URI "@uri": @msg',
|
||||
[
|
||||
'@uri' => $uri,
|
||||
'@msg' => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
return NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultSettings() {
|
||||
return [
|
||||
// 'wisski_inline' => 'FALSE',
|
||||
] + parent::defaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
|
||||
$element = parent::settingsForm($form, $form_state);
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsSummary() {
|
||||
return parent::settingsSummary();
|
||||
}
|
||||
}
|
||||
269
src/Plugin/Field/FieldFormatter/DFG3DViewerFormatter.php
Executable file
269
src/Plugin/Field/FieldFormatter/DFG3DViewerFormatter.php
Executable file
|
|
@ -0,0 +1,269 @@
|
|||
<?php
|
||||
/**
|
||||
* @file
|
||||
* Definition of Drupal\dfg_3dviewer\Plugin\field\formatter\DFG3DViewerFormatter.
|
||||
*/
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Link;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\image\Entity\ImageStyle;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatterBase;
|
||||
use Drupal\colorbox\Plugin\Field\FieldFormatter\ColorboxFormatter;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\file\Plugin\Field\FieldFormatter\FileFormatterBase;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'wisski_iip_image' formatter.
|
||||
*
|
||||
* @FieldFormatter(
|
||||
* id = "dfg_3dviewer",
|
||||
* module = "dfg_3dviewer",
|
||||
* label = @Translation("DFG 3D Viewer"),
|
||||
* field_types = {
|
||||
* "file"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
# class WisskiIIPImageFormatter extends ImageFormatterBase {
|
||||
class DFG3DViewerFormatter extends FileFormatterBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
// $elements = parent::viewElements($items, $langcode);
|
||||
$elements = array();
|
||||
$entity = $items->getEntity();
|
||||
|
||||
// By Mark:
|
||||
// get the derivative field id
|
||||
// here must be some handling if this is empty.
|
||||
$derivative_field_id = \Drupal::service('config.factory')->getEditable('dfg_3dviewer.settings')->get('dfg_3dviewer_viewer_file_name');
|
||||
|
||||
$derivative_paths = array();
|
||||
if (!empty($derivative_field_id) && $entity->hasField($derivative_field_id)) {
|
||||
$derivative_paths = $this->extractViewerPathsFromFieldValues($entity->get($derivative_field_id)->getValue());
|
||||
}
|
||||
|
||||
// if we have derivative values, act on that and not on the real values.
|
||||
if(!empty($derivative_paths)) {
|
||||
$elements = array();
|
||||
|
||||
$elements['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||
dfg_3dviewer_attach_settings($elements);
|
||||
$container_id = \Drupal::config('dfg_3dviewer.settings')->get('dfg_3dviewer_container') ?: 'DFG_3DViewer';
|
||||
\Drupal::logger('dfg_3dviewer')->notice(
|
||||
'Viewer formatter uses converted model from field "@field" for entity type "@type" id "@id".',
|
||||
[
|
||||
'@field' => (string) $derivative_field_id,
|
||||
'@type' => method_exists($entity, 'getEntityTypeId') ? (string) $entity->getEntityTypeId() : '',
|
||||
'@id' => method_exists($entity, 'id') ? (string) $entity->id() : '',
|
||||
]
|
||||
);
|
||||
|
||||
foreach($derivative_paths as $delta => $resolved_path) {
|
||||
$elements[$delta] = array(
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'p',
|
||||
'#attributes' => array('id' => $container_id, '3d' => $resolved_path),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
||||
$files = $this->getEntitiesToView($items, $langcode);
|
||||
|
||||
$elements['#attached']['library'][] = dfg_3dviewer_get_library();
|
||||
dfg_3dviewer_attach_settings($elements);
|
||||
$container_id = \Drupal::config('dfg_3dviewer.settings')->get('dfg_3dviewer_container') ?: 'DFG_3DViewer';
|
||||
\Drupal::logger('dfg_3dviewer')->notice(
|
||||
'Viewer formatter falls back to original upload field "@field" for entity type "@type" id "@id".',
|
||||
[
|
||||
'@field' => (string) $items->getName(),
|
||||
'@type' => method_exists($entity, 'getEntityTypeId') ? (string) $entity->getEntityTypeId() : '',
|
||||
'@id' => method_exists($entity, 'id') ? (string) $entity->id() : '',
|
||||
]
|
||||
);
|
||||
|
||||
foreach ($files as $delta => $file) {
|
||||
$generator = \Drupal::service('file_url_generator');
|
||||
$relative_path = (string) $generator->generateString($file->getFileUri());
|
||||
$absolute_path = (string) $generator->generateAbsoluteString($file->getFileUri());
|
||||
|
||||
$base = trim((string) \Drupal::service('config.factory')
|
||||
->getEditable('dfg_3dviewer.settings')
|
||||
->get('dfg_3dviewer_basenamespace'));
|
||||
$base_parts = parse_url($base);
|
||||
$base_host = is_array($base_parts) ? (string) ($base_parts['host'] ?? '') : '';
|
||||
$has_safe_base = is_array($base_parts)
|
||||
&& !empty($base_parts['scheme'])
|
||||
&& $base_host !== ''
|
||||
&& strpos($base_host, '_') === FALSE
|
||||
&& strtolower($base_host) !== 'default';
|
||||
|
||||
if ($has_safe_base) {
|
||||
$override_basenamespace = rtrim($base, '/') . '/' . ltrim($relative_path, '/');
|
||||
}
|
||||
else {
|
||||
$absolute_host = (string) parse_url($absolute_path, PHP_URL_HOST);
|
||||
$override_basenamespace = ((strpos($absolute_host, '_') !== FALSE) || strtolower($absolute_host) === 'default')
|
||||
? $relative_path
|
||||
: $absolute_path;
|
||||
}
|
||||
|
||||
$elements[$delta] = array(
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'p',
|
||||
'#attributes' => array('id' => $container_id, '3d' => $override_basenamespace),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $elements;
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultSettings() {
|
||||
return [
|
||||
// 'wisski_inline' => 'FALSE',
|
||||
] + parent::defaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
|
||||
/* $element['wisski_inline'] = [
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Inline mode for IIP'),
|
||||
'#default_value' => $this->getSetting('wisski_inline'),
|
||||
];
|
||||
*/
|
||||
// $element = $element + parent::settingsForm($form, $form_state);
|
||||
$element = parent::settingsForm($form, $form_state);
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsSummary() {
|
||||
return parent::settingsSummary();
|
||||
}
|
||||
|
||||
private function resolveViewerPath(string $value): string {
|
||||
$value = trim($value);
|
||||
if ($value === '') {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (preg_match('#^https?://#i', $value)) {
|
||||
$host = (string) parse_url($value, PHP_URL_HOST);
|
||||
$path = (string) parse_url($value, PHP_URL_PATH);
|
||||
if (
|
||||
$host !== ''
|
||||
&& (strpos($host, '_') !== FALSE || strtolower($host) === 'default')
|
||||
&& str_starts_with($path, '/sites/default/files/')
|
||||
) {
|
||||
$cfg = \Drupal::config('dfg_3dviewer.settings');
|
||||
$main_url = trim((string) ($cfg->get('dfg_3dviewer_main_url') ?? $cfg->get('main_url') ?? ''));
|
||||
$main_parts = parse_url($main_url);
|
||||
$main_host = is_array($main_parts) ? (string) ($main_parts['host'] ?? '') : '';
|
||||
$has_safe_main = is_array($main_parts)
|
||||
&& !empty($main_parts['scheme'])
|
||||
&& $main_host !== ''
|
||||
&& strpos($main_host, '_') === FALSE;
|
||||
return $has_safe_main ? rtrim($main_url, '/') . $path : $path;
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (str_starts_with($value, '/')) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
if (preg_match('#^[a-z][a-z0-9+.-]*://#i', $value)) {
|
||||
try {
|
||||
return \Drupal::service('file_url_generator')->generateString($value);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
\Drupal::logger('dfg_3dviewer')->warning(
|
||||
'Could not resolve stream wrapper URI "@value" for viewer path: @msg',
|
||||
[
|
||||
'@value' => $value,
|
||||
'@msg' => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
||||
if (str_starts_with($value, 'sites/default/files/')) {
|
||||
return '/' . ltrim($value, '/');
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function extractViewerPathsFromFieldValues(array $values): array {
|
||||
$paths = array();
|
||||
|
||||
foreach ($values as $delta => $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$resolved = $this->resolveViewerFieldRowPath($row);
|
||||
if ($resolved === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$paths[$delta] = $resolved;
|
||||
}
|
||||
|
||||
return $paths;
|
||||
}
|
||||
|
||||
private function resolveViewerFieldRowPath(array $row): string {
|
||||
if (!empty($row['target_id']) && ctype_digit((string) $row['target_id'])) {
|
||||
$file = \Drupal\file\Entity\File::load((int) $row['target_id']);
|
||||
if ($file) {
|
||||
try {
|
||||
$generated = (string) \Drupal::service('file_url_generator')->generateString($file->getFileUri());
|
||||
return $this->resolveViewerPath($generated);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
\Drupal::logger('dfg_3dviewer')->warning(
|
||||
'Could not resolve target_id "@target_id" for viewer formatter: @msg',
|
||||
[
|
||||
'@target_id' => (string) $row['target_id'],
|
||||
'@msg' => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (['value', 'uri'] as $key) {
|
||||
$candidate = trim((string) ($row[$key] ?? ''));
|
||||
if ($candidate !== '') {
|
||||
return $this->resolveViewerPath($candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
}
|
||||
2394
src/Plugin/QueueWorker/ConvertWorker.php
Normal file
2394
src/Plugin/QueueWorker/ConvertWorker.php
Normal file
File diff suppressed because it is too large
Load diff
301
src/Service/ConvertProcessService.php
Normal file
301
src/Service/ConvertProcessService.php
Normal file
|
|
@ -0,0 +1,301 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Service;
|
||||
|
||||
use Symfony\Component\Process\Process;
|
||||
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||
|
||||
class ConvertProcessService {
|
||||
|
||||
protected $logger;
|
||||
|
||||
public function __construct(LoggerChannelFactoryInterface $logger_factory) {
|
||||
$this->logger = $logger_factory->get('dfg_3dviewer');
|
||||
}
|
||||
|
||||
private function boolToString($value): string {
|
||||
return filter_var($value, FILTER_VALIDATE_BOOLEAN) ? 'true' : 'false';
|
||||
}
|
||||
|
||||
private function normalizePath(string $path): string {
|
||||
return rtrim(str_replace('\\', '/', $path), '/');
|
||||
}
|
||||
|
||||
private function resolveConvertedOutputPath(string $inputPath, array $options): string {
|
||||
$isBinary = filter_var($options['b'] ?? true, FILTER_VALIDATE_BOOLEAN);
|
||||
$outputExt = $isBinary ? 'glb' : 'gltf';
|
||||
$inputExt = strtolower((string) pathinfo($inputPath, PATHINFO_EXTENSION));
|
||||
|
||||
if (!empty($options['o'])) {
|
||||
$outputBase = $this->normalizePath((string) $options['o']);
|
||||
$inputBase = (string) pathinfo($inputPath, PATHINFO_FILENAME);
|
||||
return $outputBase . '/gltf/' . $inputBase . '.' . $outputExt;
|
||||
}
|
||||
|
||||
if ($inputExt === 'glb' && $outputExt === 'glb') {
|
||||
return $inputPath;
|
||||
}
|
||||
|
||||
$dirname = $this->normalizePath((string) pathinfo($inputPath, PATHINFO_DIRNAME));
|
||||
$filename = (string) pathinfo($inputPath, PATHINFO_FILENAME);
|
||||
return $dirname . '/gltf/' . $filename . '.' . $outputExt;
|
||||
}
|
||||
|
||||
private function resolveThumbnailBasePath(string $inputPath): string {
|
||||
$dirname = $this->normalizePath((string) pathinfo($inputPath, PATHINFO_DIRNAME));
|
||||
$filename = (string) pathinfo($inputPath, PATHINFO_FILENAME);
|
||||
$extension = strtolower((string) pathinfo($inputPath, PATHINFO_EXTENSION));
|
||||
|
||||
return $dirname . '/views/' . $filename . '.' . $extension;
|
||||
}
|
||||
|
||||
private function thumbnailsAlreadyExist(string $inputPath): bool {
|
||||
$basePath = $this->resolveThumbnailBasePath($inputPath);
|
||||
$requiredFiles = [
|
||||
$basePath . '_side45.png',
|
||||
$basePath . '_side90.png',
|
||||
$basePath . '_side135.png',
|
||||
$basePath . '_side180.png',
|
||||
$basePath . '_side225.png',
|
||||
$basePath . '_side270.png',
|
||||
$basePath . '_side315.png',
|
||||
$basePath . '_top.png',
|
||||
];
|
||||
|
||||
foreach ($requiredFiles as $file) {
|
||||
if (!file_exists($file)) {
|
||||
return FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
if (file_exists($basePath . '_side0.png') || file_exists($basePath . '_RENDER.png')) {
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
private function emitProgress(?callable $onProgress, int $percent, string $state, string $message): void {
|
||||
if ($onProgress === NULL) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$onProgress($percent, $state, $message);
|
||||
}
|
||||
catch (\Throwable $e) {
|
||||
$this->logger->warning(
|
||||
'Progress callback failed at @percent% (@state): @message',
|
||||
[
|
||||
'@percent' => $percent,
|
||||
'@state' => $state,
|
||||
'@message' => $e->getMessage(),
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Run convert.sh process.
|
||||
*
|
||||
* @param string $spath
|
||||
* @param string $inputPath
|
||||
* @param int $lightweight
|
||||
* @param array $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function run(
|
||||
string $spath,
|
||||
string $inputPath,
|
||||
int $lightweight = 0,
|
||||
array $options = [],
|
||||
?callable $onProgress = NULL
|
||||
) : array {
|
||||
|
||||
$script = $spath . '/scripts/convert.sh';
|
||||
|
||||
if (!file_exists($script)) {
|
||||
return [
|
||||
'success' => FALSE,
|
||||
'exit_code' => NULL,
|
||||
'output' => '',
|
||||
'error' => 'Script not found',
|
||||
];
|
||||
}
|
||||
|
||||
$args = [
|
||||
$script,
|
||||
'-t', $this->boolToString($lightweight),
|
||||
'-c', $this->boolToString($options['c'] ?? true),
|
||||
'-l', $options['l'] ?? '3',
|
||||
'-b', $this->boolToString($options['b'] ?? true),
|
||||
'-i', $inputPath,
|
||||
];
|
||||
|
||||
// optional
|
||||
if (!empty($options['o'])) {
|
||||
$args[] = '-o';
|
||||
$args[] = $options['o'];
|
||||
}
|
||||
|
||||
$args[] = '-f';
|
||||
$args[] = $this->boolToString($options['f'] ?? true);
|
||||
|
||||
if (isset($options['a'])) {
|
||||
$args[] = '-a';
|
||||
$args[] = $options['a'];
|
||||
}
|
||||
|
||||
$process = new \Symfony\Component\Process\Process($args);
|
||||
$process->setTimeout($options['timeout'] ?? 600);
|
||||
$process->setWorkingDirectory($spath);
|
||||
|
||||
$this->emitProgress($onProgress, 35, 'processing', 'Converting to GLTF...');
|
||||
$process->run();
|
||||
|
||||
$success = $process->isSuccessful();
|
||||
$exitCode = $process->getExitCode();
|
||||
$output = $process->getOutput();
|
||||
$error = $process->getErrorOutput();
|
||||
$renderResult = NULL;
|
||||
|
||||
if ($success) {
|
||||
$this->emitProgress($onProgress, 55, 'converted', 'GLTF conversion finished.');
|
||||
}
|
||||
|
||||
if ($success && !filter_var($lightweight, FILTER_VALIDATE_BOOLEAN)) {
|
||||
if ($this->thumbnailsAlreadyExist($inputPath)) {
|
||||
$this->emitProgress($onProgress, 75, 'rendering', 'Skipping thumbnail generation, files already exist.');
|
||||
}
|
||||
else {
|
||||
$this->emitProgress($onProgress, 65, 'rendering', 'Generating thumbnails...');
|
||||
$renderResult = $this->render(
|
||||
$spath,
|
||||
$inputPath,
|
||||
[
|
||||
'a' => $this->boolToString($options['a'] ?? false),
|
||||
'g' => $this->resolveConvertedOutputPath($inputPath, $options),
|
||||
'timeout' => $options['render_timeout'] ?? $options['timeout'] ?? 600,
|
||||
]
|
||||
);
|
||||
|
||||
$output .= $renderResult['output'] ?? '';
|
||||
$error .= $renderResult['error'] ?? '';
|
||||
|
||||
if (!($renderResult['success'] ?? FALSE)) {
|
||||
$success = FALSE;
|
||||
$exitCode = $renderResult['exit_code'] ?? 1;
|
||||
}
|
||||
else {
|
||||
$this->emitProgress($onProgress, 75, 'rendering', 'Thumbnails generated.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => $success,
|
||||
'exit_code' => $exitCode,
|
||||
'output' => $output,
|
||||
'error' => $error,
|
||||
'command' => $process->getCommandLine(),
|
||||
'render' => $renderResult,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Run render.sh process.
|
||||
*
|
||||
* @param string $spath
|
||||
* @param string $inputPath
|
||||
* @param array $options
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function render(
|
||||
string $spath,
|
||||
string $inputPath,
|
||||
array $options = []
|
||||
) : array {
|
||||
$script = $spath . '/scripts/render.sh';
|
||||
|
||||
if (!file_exists($script)) {
|
||||
return [
|
||||
'success' => FALSE,
|
||||
'exit_code' => NULL,
|
||||
'output' => '',
|
||||
'error' => 'Script not found',
|
||||
];
|
||||
}
|
||||
|
||||
$args = [
|
||||
$script,
|
||||
'-i', $inputPath,
|
||||
'-a', $this->boolToString($options['a'] ?? false),
|
||||
];
|
||||
|
||||
if (!empty($options['g'])) {
|
||||
$args[] = '-g';
|
||||
$args[] = $options['g'];
|
||||
}
|
||||
|
||||
$process = new \Symfony\Component\Process\Process($args);
|
||||
$process->setTimeout($options['timeout'] ?? 600);
|
||||
$process->setWorkingDirectory($spath);
|
||||
$process->run();
|
||||
|
||||
return [
|
||||
'success' => $process->isSuccessful(),
|
||||
'exit_code' => $process->getExitCode(),
|
||||
'output' => $process->getOutput(),
|
||||
'error' => $process->getErrorOutput(),
|
||||
'command' => $process->getCommandLine(),
|
||||
];
|
||||
}
|
||||
|
||||
public function uncompress(
|
||||
string $spath,
|
||||
string $type,
|
||||
string $inputPath,
|
||||
string $outputPath,
|
||||
string $name,
|
||||
array $options = []
|
||||
) : array {
|
||||
|
||||
$script = $spath . '/scripts/uncompress.sh';
|
||||
|
||||
if (!file_exists($script)) {
|
||||
return [
|
||||
'success' => FALSE,
|
||||
'exit_code' => NULL,
|
||||
'output' => '',
|
||||
'error' => 'Script not found',
|
||||
];
|
||||
}
|
||||
|
||||
$args = [
|
||||
$script,
|
||||
'-t', $type,
|
||||
'-i', $inputPath,
|
||||
'-o', $outputPath,
|
||||
'-n', $name,
|
||||
];
|
||||
|
||||
$process = new \Symfony\Component\Process\Process($args);
|
||||
$process->setTimeout($options['timeout'] ?? 600);
|
||||
$process->setWorkingDirectory($spath);
|
||||
|
||||
$process->run();
|
||||
|
||||
return [
|
||||
'success' => $process->isSuccessful(),
|
||||
'exit_code' => $process->getExitCode(),
|
||||
'output' => $process->getOutput(),
|
||||
'error' => $process->getErrorOutput(),
|
||||
'command' => $process->getCommandLine(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
28
src/Service/ModelFormatManager.php
Normal file
28
src/Service/ModelFormatManager.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\dfg_3dviewer\Service;
|
||||
|
||||
class ModelFormatManager {
|
||||
|
||||
protected array $allowedModelFormats = [
|
||||
'abc', 'obj', 'fbx', 'ply', 'dae', 'ifc',
|
||||
'stl', 'xyz', 'pcd', 'json', '3ds',
|
||||
'blend', 'gml', 'wrl', 'glb', 'gltf'
|
||||
];
|
||||
|
||||
protected array $zipFormats = [
|
||||
'zip', 'rar', 'tar', 'xz', 'gz'
|
||||
];
|
||||
|
||||
public function getAllowedModelFormats(): array {
|
||||
return $this->allowedModelFormats;
|
||||
}
|
||||
|
||||
public function getZipFormats(): array {
|
||||
return $this->zipFormats;
|
||||
}
|
||||
|
||||
public function getAllFormats(): array {
|
||||
return array_merge($this->allowedModelFormats, $this->zipFormats);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue