Initial commit

This commit is contained in:
Robert Nasarek 2026-06-25 09:09:16 +02:00
commit a437c068c8
64 changed files with 561683 additions and 0 deletions

View 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;
}
}
}

View 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;
}
}

View 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',
]);
}
}

View 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
]);
}
}

View 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;
}
}

View 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');
}
}

View 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();
}
}

View 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 '';
}
}

File diff suppressed because it is too large Load diff

View 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(),
];
}
}

View 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);
}
}