revision->project_id === $project->id, 404); abort_unless($request->user()->can('render', [$file, $project]), 403); if ($file->extension !== '3mf') { return back()->withErrors(['file' => 'Product image generation is only supported for 3MF files.'])->setStatusCode(422); } if ($file->render !== null) { return redirect()->route('projects.show', $project)->with('status', 'Product image already generated for this file.'); } $sourceDisk = Storage::disk('project_files'); if (! $sourceDisk->exists($file->storage_key)) { return back()->withErrors(['file' => 'The original file could not be found.'])->setStatusCode(404); } $renderDisk = Storage::disk('project_renders'); $storedKey = null; try { return DB::transaction(function () use ($project, $file, $request, $sourceDisk, $renderDisk, &$storedKey): RedirectResponse { Project::query()->whereKey($project->id)->lockForUpdate()->firstOrFail(); if (ProjectFileRender::where('project_file_id', $file->id)->exists()) { return redirect()->route('projects.show', $project)->with('status', 'Product image already generated for this file.'); } $scene = app(ThreeMfGeometryExtractor::class)->extract($sourceDisk->path($file->storage_key)); $rendered = app(ProductImageRenderer::class)->render($scene['triangles']); $extension = $rendered['renderer'] === ProductImageRenderer::RENDERER_GD ? 'png' : 'svg'; $key = bin2hex(random_bytes(20)).'.'.$extension; $storedKey = $key; if (! $renderDisk->put($key, $rendered['bytes'])) { throw new \RuntimeException('Unable to store the generated product image.'); } $altText = sprintf( 'Numeric isometric render of %s: %d triangle%s across %d instance%s, rendered as %s.', $file->original_filename, $scene['triangle_count'], $scene['triangle_count'] === 1 ? '' : 's', $scene['instance_count'], $scene['instance_count'] === 1 ? '' : 's', strtoupper($extension), ); $render = ProjectFileRender::create([ 'project_file_id' => $file->id, 'renderer' => $rendered['renderer'], 'mime_type' => $rendered['mime'], 'storage_key' => $key, 'width' => $rendered['width'], 'height' => $rendered['height'], 'triangle_count' => $scene['triangle_count'], 'instance_count' => $scene['instance_count'], 'alt_text' => $altText, 'byte_size' => strlen($rendered['bytes']), 'sha256' => hash('sha256', $rendered['bytes']), 'generated_by' => $request->user()->id, 'generated_at' => now(), ]); ProjectEvent::record($project->id, 'product_image_generated', $request->user()->id, ProjectFile::class, $file->id, [ 'renderer' => $render->renderer, 'triangle_count' => $render->triangle_count, 'instance_count' => $render->instance_count, ]); return redirect()->route('projects.show', $project)->with('status', 'Product image generated.'); }); } catch (Throwable $exception) { if ($storedKey !== null) { $renderDisk->delete($storedKey); } report($exception); return back()->withErrors(['file' => 'Unable to generate a product image: '.$exception->getMessage()])->setStatusCode(422); } } public function preview(Request $request, Project $project, ProjectFile $file): Response { abort_unless($file->revision->project_id === $project->id, 404); abort_unless($request->user()->can('render', [$file, $project]), 403); $render = $file->render; abort_if($render === null, 404); return response(Storage::disk('project_renders')->get($render->storage_key)) ->header('Content-Type', $render->mime_type) ->header('Content-Disposition', 'inline'); } public function download(Request $request, Project $project, ProjectFile $file): StreamedResponse { abort_unless($file->revision->project_id === $project->id, 404); abort_unless($request->user()->can('render', [$file, $project]), 403); $render = $file->render; abort_if($render === null, 404); $extension = $render->mime_type === 'image/png' ? 'png' : 'svg'; return Storage::disk('project_renders')->download($render->storage_key, $file->original_filename.'.'.$extension); } }