documents preview - image

This commit is contained in:
2026-03-11 09:40:03 +01:00
parent 0e4c79b735
commit 11cc5350c8
8 changed files with 176 additions and 140 deletions

View File

@@ -235,17 +235,14 @@
<a class="bubble-download" [href]="entry.downloadUrl" [download]="entry.fileName">Download</a> <a class="bubble-download" [href]="entry.downloadUrl" [download]="entry.fileName">Download</a>
} }
@if (hasPdfPreview(entry)) { @if (hasDocumentPreviewImage(entry)) {
<div class="bubble-preview"> <div class="bubble-preview">
<div class="bubble-preview-label">Preview</div> <div class="bubble-preview-label">Preview</div>
@defer (on viewport) { <img
<app-office-pdf-preview class="bubble-preview-image"
[src]="pdfPreviewUrl(entry)!" [src]="documentPreviewImageUrl(entry)"
[fileName]="entry.fileName ?? 'document.pdf'" [alt]="entry.fileName || 'Document preview'"
></app-office-pdf-preview> />
} @placeholder {
<div class="bubble-preview-placeholder">Loading preview…</div>
}
</div> </div>
} }
</div> </div>

View File

@@ -538,17 +538,14 @@
opacity: 0.78; opacity: 0.78;
} }
.bubble-preview-placeholder { .bubble-preview-image {
display: grid; display: block;
place-items: center;
width: min(240px, 100%); width: min(240px, 100%);
min-height: 320px; max-width: 100%;
padding: 1rem; height: auto;
border: 1px dashed var(--input-border); border: 1px solid var(--surface-border);
border-radius: 1rem; border-radius: 1rem;
color: var(--page-text-soft); background: #fff;
background: color-mix(in srgb, var(--surface-background) 85%, white);
text-align: center;
} }
.bubble-image, .bubble-image,

View File

@@ -3,7 +3,6 @@ import { Component, computed, effect, ElementRef, inject, NgZone, OnDestroy, sig
import { toSignal } from '@angular/core/rxjs-interop'; import { toSignal } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { OfficePdfPreviewComponent } from './office-pdf-preview.component';
import { PeerCallModalComponent } from './peer-call-modal.component'; import { PeerCallModalComponent } from './peer-call-modal.component';
import { ChatSessionService } from './chat-session.service'; import { ChatSessionService } from './chat-session.service';
@@ -17,7 +16,6 @@ import type { CallMode, ChatEntry, ConnectionState, PeerSummary } from './models
FormsModule, FormsModule,
RouterLink, RouterLink,
JsonFileViewerComponent, JsonFileViewerComponent,
OfficePdfPreviewComponent,
PeerCallModalComponent, PeerCallModalComponent,
], ],
templateUrl: './chat-page.component.html', templateUrl: './chat-page.component.html',
@@ -646,31 +644,20 @@ export class ChatPageComponent implements OnDestroy {
); );
} }
hasPdfPreview(entry: ChatEntry): boolean { hasDocumentPreviewImage(entry: ChatEntry): boolean {
return ( return (
entry.kind === 'file' && entry.kind === 'file' &&
( !!entry.previewDownloadUrl &&
( (entry.previewMimeType?.startsWith('image/') ?? false)
entry.previewMimeType === 'application/pdf' &&
!!entry.previewDownloadUrl
) ||
(
!!entry.downloadUrl &&
(
entry.fileMimeType === 'application/pdf' ||
entry.fileName?.toLowerCase().endsWith('.pdf') === true
)
)
)
); );
} }
pdfPreviewUrl(entry: ChatEntry): string | null { documentPreviewImageUrl(entry: ChatEntry): string | null {
if (!this.hasPdfPreview(entry)) { if (!this.hasDocumentPreviewImage(entry)) {
return null; return null;
} }
return entry.previewDownloadUrl ?? entry.downloadUrl ?? null; return entry.previewDownloadUrl ?? null;
} }
isPeerTyping(peerId: string): boolean { isPeerTyping(peerId: string): boolean {

View File

@@ -98,9 +98,9 @@ type PersistedChatEntryContent = {
previewMimeType?: string; previewMimeType?: string;
}; };
type OfficePreviewResponse = { type DocumentPreviewImageResponse = {
mimeType: string; mimeType: string;
pdfBase64: string; imageBase64: string;
}; };
type RuntimeEnv = { type RuntimeEnv = {
@@ -1586,13 +1586,13 @@ export class ChatSessionService {
let previewMimeType: string | undefined; let previewMimeType: string | undefined;
let previewDownloadUrl: string | undefined; let previewDownloadUrl: string | undefined;
if (transfer.kind === 'file' && this.isOfficeDocumentFile(transfer.name, transfer.mimeType)) { if (transfer.kind === 'file' && this.isPreviewableDocumentFile(transfer.name, transfer.mimeType)) {
const officePreview = await this.generateOfficeDocumentPreview(transfer.name, blob); const imagePreview = await this.generateDocumentPreviewImage(transfer.name, blob);
if (officePreview) { if (imagePreview) {
previewBlob = officePreview.blob; previewBlob = imagePreview.blob;
previewMimeType = officePreview.mimeType; previewMimeType = imagePreview.mimeType;
previewDownloadUrl = URL.createObjectURL(officePreview.blob); previewDownloadUrl = URL.createObjectURL(imagePreview.blob);
} }
} }
@@ -2287,7 +2287,7 @@ export class ChatSessionService {
Uint8Array.from(entry.previewIv).buffer, Uint8Array.from(entry.previewIv).buffer,
); );
const previewBlob = new Blob([decryptedPreview], { const previewBlob = new Blob([decryptedPreview], {
type: content.previewMimeType || 'application/pdf', type: content.previewMimeType || 'image/png',
}); });
previewDownloadUrl = URL.createObjectURL(previewBlob); previewDownloadUrl = URL.createObjectURL(previewBlob);
} }
@@ -2344,7 +2344,7 @@ export class ChatSessionService {
: undefined; : undefined;
const previewBlob = entry.previewBlob const previewBlob = entry.previewBlob
? new Blob([await entry.previewBlob.arrayBuffer()], { ? new Blob([await entry.previewBlob.arrayBuffer()], {
type: entry.previewMimeType || 'application/pdf', type: entry.previewMimeType || 'image/png',
}) })
: undefined; : undefined;
@@ -2931,7 +2931,7 @@ export class ChatSessionService {
return new Blob([bytes], { type: mimeType }); return new Blob([bytes], { type: mimeType });
} }
private async generateOfficeDocumentPreview( private async generateDocumentPreviewImage(
fileName: string, fileName: string,
fileBlob: Blob, fileBlob: Blob,
): Promise<{ blob: Blob; mimeType: string } | null> { ): Promise<{ blob: Blob; mimeType: string } | null> {
@@ -2943,8 +2943,8 @@ export class ChatSessionService {
try { try {
const response = await firstValueFrom( const response = await firstValueFrom(
this.http.post<OfficePreviewResponse>( this.http.post<DocumentPreviewImageResponse>(
`${this.serverUrl()}/api/files/office-preview`, `${this.serverUrl()}/api/files/document-preview-image`,
{ {
fileName, fileName,
mimeType: fileBlob.type || 'application/octet-stream', mimeType: fileBlob.type || 'application/octet-stream',
@@ -2957,15 +2957,19 @@ export class ChatSessionService {
); );
return { return {
blob: this.base64ToBlob(response.pdfBase64, response.mimeType), blob: this.base64ToBlob(response.imageBase64, response.mimeType),
mimeType: response.mimeType, mimeType: response.mimeType,
}; };
} catch (error) { } catch (error) {
console.warn('Could not generate office document preview.', error); console.warn('Could not generate document preview image.', error);
return null; return null;
} }
} }
private isPreviewableDocumentFile(fileName?: string, mimeType?: string): boolean {
return this.isOfficeDocumentFile(fileName, mimeType) || this.isPdfFile(fileName, mimeType);
}
private isOfficeDocumentFile(fileName?: string, mimeType?: string): boolean { private isOfficeDocumentFile(fileName?: string, mimeType?: string): boolean {
const normalizedName = fileName?.trim().toLowerCase() ?? ''; const normalizedName = fileName?.trim().toLowerCase() ?? '';
const normalizedMimeType = mimeType?.trim().toLowerCase() ?? ''; const normalizedMimeType = mimeType?.trim().toLowerCase() ?? '';
@@ -2981,6 +2985,13 @@ export class ChatSessionService {
return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedName); return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedName);
} }
private isPdfFile(fileName?: string, mimeType?: string): boolean {
const normalizedName = fileName?.trim().toLowerCase() ?? '';
const normalizedMimeType = mimeType?.trim().toLowerCase() ?? '';
return normalizedMimeType === 'application/pdf' || normalizedName.endsWith('.pdf');
}
private fileExtensionForMimeType(mimeType: string): string { private fileExtensionForMimeType(mimeType: string): string {
const normalizedMimeType = mimeType.split(';', 1)[0]?.trim().toLowerCase() || 'application/octet-stream'; const normalizedMimeType = mimeType.split(';', 1)[0]?.trim().toLowerCase() || 'application/octet-stream';

View File

@@ -1,13 +0,0 @@
:host {
display: block;
width: min(240px, 100%);
}
.office-pdf-preview-shell {
width: min(240px, 100%);
overflow: hidden;
border: 1px solid var(--surface-border);
border-radius: 1rem;
background: #fff;
box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.16);
}

View File

@@ -1,45 +0,0 @@
import { Component, Input } from '@angular/core';
import { NgxExtendedPdfViewerModule } from 'ngx-extended-pdf-viewer';
@Component({
selector: 'app-office-pdf-preview',
imports: [NgxExtendedPdfViewerModule],
template: `
<div class="office-pdf-preview-shell">
<ngx-extended-pdf-viewer
[src]="src"
[filenameForDownload]="fileName"
[height]="'320px'"
[page]="1"
[zoom]="'page-width'"
[showToolbar]="false"
[showSidebarButton]="false"
[showFindButton]="false"
[showPagingButtons]="false"
[showPageNumber]="false"
[showPageLabel]="false"
[showZoomButtons]="false"
[showZoomDropdown]="false"
[showPresentationModeButton]="false"
[showOpenFileButton]="false"
[showPrintButton]="false"
[showDownloadButton]="false"
[showSecondaryToolbarButton]="false"
[showRotateButton]="false"
[showRotateCwButton]="false"
[showRotateCcwButton]="false"
[showScrollingButtons]="false"
[showSpreadButton]="false"
[showPropertiesButton]="false"
[showHandToolButton]="false"
[showBorders]="false"
[textLayer]="false"
></ngx-extended-pdf-viewer>
</div>
`,
styleUrl: './office-pdf-preview.component.scss',
})
export class OfficePdfPreviewComponent {
@Input({ required: true }) src!: string;
@Input() fileName = 'document.pdf';
}

72
server/dist/index.js vendored
View File

@@ -1,5 +1,7 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { execFile } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { promisify, TextEncoder } from 'node:util'; import { promisify, TextEncoder } from 'node:util';
@@ -48,7 +50,7 @@ const adminDeleteUserParamsSchema = z.object({
const webBundleFileParamsSchema = z.object({ const webBundleFileParamsSchema = z.object({
'*': z.string().min(1), '*': z.string().min(1),
}); });
const officePreviewSchema = z.object({ const documentPreviewSchema = z.object({
fileName: z.string().trim().min(1).max(256), fileName: z.string().trim().min(1).max(256),
mimeType: z.string().trim().min(1).max(256), mimeType: z.string().trim().min(1).max(256),
fileBase64: z.string().min(1).max(96_000_000), fileBase64: z.string().min(1).max(96_000_000),
@@ -118,6 +120,7 @@ const webAuthnUserVerification = resolveWebAuthnUserVerification(process.env.WEB
const frontendIndexPath = path.join(frontendDistPath, 'index.html'); const frontendIndexPath = path.join(frontendDistPath, 'index.html');
const hasFrontendBuild = fs.existsSync(frontendIndexPath); const hasFrontendBuild = fs.existsSync(frontendIndexPath);
const convertOfficeDocument = promisify(libreOffice.convertWithOptions); const convertOfficeDocument = promisify(libreOffice.convertWithOptions);
const execFileAsync = promisify(execFile);
const speechTranscriber = new SpeechTranscriber({ const speechTranscriber = new SpeechTranscriber({
serviceUrl: speechTranscriptionServiceUrl, serviceUrl: speechTranscriptionServiceUrl,
language: speechTranscriptionLanguage, language: speechTranscriptionLanguage,
@@ -469,32 +472,32 @@ app.get('/api/auth/session', async (request, reply) => {
messageEncryptionKey: authContext.user.messageEncryptionKey, messageEncryptionKey: authContext.user.messageEncryptionKey,
}; };
}); });
app.post('/api/files/office-preview', { bodyLimit: 64 * 1024 * 1024 }, async (request, reply) => { app.post('/api/files/document-preview-image', { bodyLimit: 64 * 1024 * 1024 }, async (request, reply) => {
const authContext = await authenticateRequest(request, reply); const authContext = await authenticateRequest(request, reply);
if (!authContext) { if (!authContext) {
return; return;
} }
const parsed = officePreviewSchema.safeParse(request.body); const parsed = documentPreviewSchema.safeParse(request.body);
if (!parsed.success) { if (!parsed.success) {
return reply.code(400).send({ return reply.code(400).send({
message: 'Invalid office preview payload.', message: 'Invalid document preview payload.',
issues: parsed.error.flatten(), issues: parsed.error.flatten(),
}); });
} }
if (!isSupportedOfficeDocument(parsed.data.fileName, parsed.data.mimeType)) { if (!isSupportedPreviewDocument(parsed.data.fileName, parsed.data.mimeType)) {
return reply.code(400).send({ message: 'Only DOCX, XLSX, and PPTX files can be previewed.' }); return reply.code(400).send({ message: 'Only PDF, DOCX, XLSX, and PPTX files can be previewed.' });
} }
try { try {
const pdfBuffer = await convertOfficeDocumentToPdf(parsed.data.fileName, parsed.data.fileBase64); const previewImageBuffer = await createDocumentPreviewImage(parsed.data.fileName, parsed.data.mimeType, parsed.data.fileBase64);
return { return {
mimeType: 'application/pdf', mimeType: 'image/png',
pdfBase64: pdfBuffer.toString('base64'), imageBase64: previewImageBuffer.toString('base64'),
}; };
} }
catch (error) { catch (error) {
app.log.warn({ err: error, userId: authContext.user.id }, 'Office preview generation failed'); app.log.warn({ err: error, userId: authContext.user.id }, 'Document preview generation failed');
return reply.code(422).send({ return reply.code(422).send({
message: describeOfficePreviewFailure(error), message: describeDocumentPreviewFailure(error),
}); });
} }
}); });
@@ -879,6 +882,40 @@ async function convertOfficeDocumentToPdf(fileName, fileBase64) {
const normalizedFileName = normalizeOfficeDocumentFileName(fileName); const normalizedFileName = normalizeOfficeDocumentFileName(fileName);
return convertOfficeDocument(inputBuffer, '.pdf', undefined, { fileName: normalizedFileName }); return convertOfficeDocument(inputBuffer, '.pdf', undefined, { fileName: normalizedFileName });
} }
async function createDocumentPreviewImage(fileName, mimeType, fileBase64) {
const normalizedMimeType = mimeType.trim().toLowerCase();
const pdfBuffer = normalizedMimeType === 'application/pdf'
? decodeBase64File(fileBase64, 'The uploaded PDF is empty.')
: await convertOfficeDocumentToPdf(fileName, fileBase64);
return renderPdfFirstPageToPng(pdfBuffer);
}
async function renderPdfFirstPageToPng(pdfBuffer) {
const tempDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'privatechat-preview-'));
const pdfPath = path.join(tempDirectory, 'source.pdf');
const outputBasePath = path.join(tempDirectory, 'page-preview');
const imagePath = `${outputBasePath}.png`;
try {
await fs.promises.writeFile(pdfPath, pdfBuffer);
await execFileAsync('pdftoppm', ['-png', '-f', '1', '-singlefile', pdfPath, outputBasePath]);
return await fs.promises.readFile(imagePath);
}
finally {
await fs.promises.rm(tempDirectory, { recursive: true, force: true });
}
}
function decodeBase64File(fileBase64, emptyMessage) {
const inputBuffer = Buffer.from(fileBase64, 'base64');
if (inputBuffer.byteLength === 0) {
throw new Error(emptyMessage);
}
return inputBuffer;
}
function isSupportedPreviewDocument(fileName, mimeType) {
if (isPdfFile(fileName, mimeType)) {
return true;
}
return isSupportedOfficeDocument(fileName, mimeType);
}
function isSupportedOfficeDocument(fileName, mimeType) { function isSupportedOfficeDocument(fileName, mimeType) {
const normalizedFileName = fileName.trim().toLowerCase(); const normalizedFileName = fileName.trim().toLowerCase();
const normalizedMimeType = mimeType.trim().toLowerCase(); const normalizedMimeType = mimeType.trim().toLowerCase();
@@ -889,17 +926,22 @@ function isSupportedOfficeDocument(fileName, mimeType) {
} }
return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedFileName); return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedFileName);
} }
function isPdfFile(fileName, mimeType) {
const normalizedFileName = fileName.trim().toLowerCase();
const normalizedMimeType = mimeType.trim().toLowerCase();
return normalizedMimeType === 'application/pdf' || normalizedFileName.endsWith('.pdf');
}
function normalizeOfficeDocumentFileName(fileName) { function normalizeOfficeDocumentFileName(fileName) {
return fileName.trim().replace(/\.xslx$/i, '.xlsx'); return fileName.trim().replace(/\.xslx$/i, '.xlsx');
} }
function describeOfficePreviewFailure(error) { function describeDocumentPreviewFailure(error) {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
return 'Office preview generation failed because LibreOffice is not installed on the server.'; return 'Document preview generation failed because a required conversion tool is missing on the server.';
} }
if (error instanceof Error && error.message.trim()) { if (error instanceof Error && error.message.trim()) {
return `Office preview generation failed: ${error.message}`; return `Document preview generation failed: ${error.message}`;
} }
return 'Office preview generation failed.'; return 'Document preview generation failed.';
} }
function createUser(input) { function createUser(input) {
const createdAt = new Date().toISOString(); const createdAt = new Date().toISOString();

View File

@@ -1,5 +1,7 @@
import crypto from 'node:crypto'; import crypto from 'node:crypto';
import { execFile } from 'node:child_process';
import fs from 'node:fs'; import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { promisify, TextEncoder } from 'node:util'; import { promisify, TextEncoder } from 'node:util';
@@ -272,7 +274,7 @@ const webBundleFileParamsSchema = z.object({
'*': z.string().min(1), '*': z.string().min(1),
}); });
const officePreviewSchema = z.object({ const documentPreviewSchema = z.object({
fileName: z.string().trim().min(1).max(256), fileName: z.string().trim().min(1).max(256),
mimeType: z.string().trim().min(1).max(256), mimeType: z.string().trim().min(1).max(256),
fileBase64: z.string().min(1).max(96_000_000), fileBase64: z.string().min(1).max(96_000_000),
@@ -354,6 +356,7 @@ const webAuthnUserVerification = resolveWebAuthnUserVerification(
const frontendIndexPath = path.join(frontendDistPath, 'index.html'); const frontendIndexPath = path.join(frontendDistPath, 'index.html');
const hasFrontendBuild = fs.existsSync(frontendIndexPath); const hasFrontendBuild = fs.existsSync(frontendIndexPath);
const convertOfficeDocument = promisify(libreOffice.convertWithOptions); const convertOfficeDocument = promisify(libreOffice.convertWithOptions);
const execFileAsync = promisify(execFile);
const speechTranscriber = new SpeechTranscriber( const speechTranscriber = new SpeechTranscriber(
{ {
@@ -803,37 +806,41 @@ app.get('/api/auth/session', async (request, reply) => {
}; };
}); });
app.post('/api/files/office-preview', { bodyLimit: 64 * 1024 * 1024 }, async (request, reply) => { app.post('/api/files/document-preview-image', { bodyLimit: 64 * 1024 * 1024 }, async (request, reply) => {
const authContext = await authenticateRequest(request, reply); const authContext = await authenticateRequest(request, reply);
if (!authContext) { if (!authContext) {
return; return;
} }
const parsed = officePreviewSchema.safeParse(request.body); const parsed = documentPreviewSchema.safeParse(request.body);
if (!parsed.success) { if (!parsed.success) {
return reply.code(400).send({ return reply.code(400).send({
message: 'Invalid office preview payload.', message: 'Invalid document preview payload.',
issues: parsed.error.flatten(), issues: parsed.error.flatten(),
}); });
} }
if (!isSupportedOfficeDocument(parsed.data.fileName, parsed.data.mimeType)) { if (!isSupportedPreviewDocument(parsed.data.fileName, parsed.data.mimeType)) {
return reply.code(400).send({ message: 'Only DOCX, XLSX, and PPTX files can be previewed.' }); return reply.code(400).send({ message: 'Only PDF, DOCX, XLSX, and PPTX files can be previewed.' });
} }
try { try {
const pdfBuffer = await convertOfficeDocumentToPdf(parsed.data.fileName, parsed.data.fileBase64); const previewImageBuffer = await createDocumentPreviewImage(
parsed.data.fileName,
parsed.data.mimeType,
parsed.data.fileBase64,
);
return { return {
mimeType: 'application/pdf', mimeType: 'image/png',
pdfBase64: pdfBuffer.toString('base64'), imageBase64: previewImageBuffer.toString('base64'),
}; };
} catch (error) { } catch (error) {
app.log.warn({ err: error, userId: authContext.user.id }, 'Office preview generation failed'); app.log.warn({ err: error, userId: authContext.user.id }, 'Document preview generation failed');
return reply.code(422).send({ return reply.code(422).send({
message: describeOfficePreviewFailure(error), message: describeDocumentPreviewFailure(error),
}); });
} }
}); });
@@ -1348,6 +1355,52 @@ async function convertOfficeDocumentToPdf(fileName: string, fileBase64: string):
return convertOfficeDocument(inputBuffer, '.pdf', undefined, { fileName: normalizedFileName }); return convertOfficeDocument(inputBuffer, '.pdf', undefined, { fileName: normalizedFileName });
} }
async function createDocumentPreviewImage(
fileName: string,
mimeType: string,
fileBase64: string,
): Promise<Buffer> {
const normalizedMimeType = mimeType.trim().toLowerCase();
const pdfBuffer = normalizedMimeType === 'application/pdf'
? decodeBase64File(fileBase64, 'The uploaded PDF is empty.')
: await convertOfficeDocumentToPdf(fileName, fileBase64);
return renderPdfFirstPageToPng(pdfBuffer);
}
async function renderPdfFirstPageToPng(pdfBuffer: Buffer): Promise<Buffer> {
const tempDirectory = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'privatechat-preview-'));
const pdfPath = path.join(tempDirectory, 'source.pdf');
const outputBasePath = path.join(tempDirectory, 'page-preview');
const imagePath = `${outputBasePath}.png`;
try {
await fs.promises.writeFile(pdfPath, pdfBuffer);
await execFileAsync('pdftoppm', ['-png', '-f', '1', '-singlefile', pdfPath, outputBasePath]);
return await fs.promises.readFile(imagePath);
} finally {
await fs.promises.rm(tempDirectory, { recursive: true, force: true });
}
}
function decodeBase64File(fileBase64: string, emptyMessage: string): Buffer {
const inputBuffer = Buffer.from(fileBase64, 'base64');
if (inputBuffer.byteLength === 0) {
throw new Error(emptyMessage);
}
return inputBuffer;
}
function isSupportedPreviewDocument(fileName: string, mimeType: string): boolean {
if (isPdfFile(fileName, mimeType)) {
return true;
}
return isSupportedOfficeDocument(fileName, mimeType);
}
function isSupportedOfficeDocument(fileName: string, mimeType: string): boolean { function isSupportedOfficeDocument(fileName: string, mimeType: string): boolean {
const normalizedFileName = fileName.trim().toLowerCase(); const normalizedFileName = fileName.trim().toLowerCase();
const normalizedMimeType = mimeType.trim().toLowerCase(); const normalizedMimeType = mimeType.trim().toLowerCase();
@@ -1363,20 +1416,27 @@ function isSupportedOfficeDocument(fileName: string, mimeType: string): boolean
return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedFileName); return /\.(docx|xlsx|xslx|pptx)$/i.test(normalizedFileName);
} }
function isPdfFile(fileName: string, mimeType: string): boolean {
const normalizedFileName = fileName.trim().toLowerCase();
const normalizedMimeType = mimeType.trim().toLowerCase();
return normalizedMimeType === 'application/pdf' || normalizedFileName.endsWith('.pdf');
}
function normalizeOfficeDocumentFileName(fileName: string): string { function normalizeOfficeDocumentFileName(fileName: string): string {
return fileName.trim().replace(/\.xslx$/i, '.xlsx'); return fileName.trim().replace(/\.xslx$/i, '.xlsx');
} }
function describeOfficePreviewFailure(error: unknown): string { function describeDocumentPreviewFailure(error: unknown): string {
if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') { if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
return 'Office preview generation failed because LibreOffice is not installed on the server.'; return 'Document preview generation failed because a required conversion tool is missing on the server.';
} }
if (error instanceof Error && error.message.trim()) { if (error instanceof Error && error.message.trim()) {
return `Office preview generation failed: ${error.message}`; return `Document preview generation failed: ${error.message}`;
} }
return 'Office preview generation failed.'; return 'Document preview generation failed.';
} }
function createUser(input: { function createUser(input: {