diff --git a/apps/client/src/ee/features.ts b/apps/client/src/ee/features.ts
index a9ab8b0d..cacf851f 100644
--- a/apps/client/src/ee/features.ts
+++ b/apps/client/src/ee/features.ts
@@ -8,6 +8,7 @@ export const Feature = {
AI: 'ai',
CONFLUENCE_IMPORT: 'import:confluence',
DOCX_IMPORT: 'import:docx',
+ PDF_IMPORT: 'import:pdf',
ATTACHMENT_INDEXING: 'attachment:indexing',
SECURITY_SETTINGS: 'security:settings',
MCP: 'mcp',
diff --git a/apps/client/src/features/page/components/page-import-modal.tsx b/apps/client/src/features/page/components/page-import-modal.tsx
index df6691d5..087beeac 100644
--- a/apps/client/src/features/page/components/page-import-modal.tsx
+++ b/apps/client/src/features/page/components/page-import-modal.tsx
@@ -12,6 +12,7 @@ import {
IconCheck,
IconFileCode,
IconFileTypeDocx,
+ IconFileTypePdf,
IconFileTypeZip,
IconMarkdown,
IconX,
@@ -90,12 +91,14 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
const markdownFileRef = useRef<() => void>(null);
const htmlFileRef = useRef<() => void>(null);
const docxFileRef = useRef<() => void>(null);
+ const pdfFileRef = useRef<() => void>(null);
const notionFileRef = useRef<() => void>(null);
const confluenceFileRef = useRef<() => void>(null);
const zipFileRef = useRef<() => void>(null);
const canUseConfluence = useHasFeature(Feature.CONFLUENCE_IMPORT);
const canUseDocx = useHasFeature(Feature.DOCX_IMPORT);
+ const canUsePdf = useHasFeature(Feature.PDF_IMPORT);
const upgradeLabel = useUpgradeLabel();
const handleZipUpload = async (selectedFile: File, source: string) => {
@@ -298,6 +301,7 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
if (markdownFileRef.current) markdownFileRef.current();
if (htmlFileRef.current) htmlFileRef.current();
if (docxFileRef.current) docxFileRef.current();
+ if (pdfFileRef.current) pdfFileRef.current();
const pageCountText =
pageCount === 1 ? `1 ${t("page")}` : `${pageCount} ${t("pages")}`;
@@ -378,6 +382,30 @@ function ImportFormatSelection({ spaceId, onClose }: ImportFormatSelection) {
)}
+
+ {(props) => (
+
+ }
+ {...props}
+ >
+ PDF
+
+
+ )}
+
+
handleZipUpload(file, "notion")}
accept="application/zip"
diff --git a/apps/server/src/common/features.ts b/apps/server/src/common/features.ts
index 38f226a8..c5fd9a20 100644
--- a/apps/server/src/common/features.ts
+++ b/apps/server/src/common/features.ts
@@ -8,6 +8,7 @@ export const Feature = {
AI: 'ai',
CONFLUENCE_IMPORT: 'import:confluence',
DOCX_IMPORT: 'import:docx',
+ PDF_IMPORT: 'import:pdf',
ATTACHMENT_INDEXING: 'attachment:indexing',
SECURITY_SETTINGS: 'security:settings',
MCP: 'mcp',
diff --git a/apps/server/src/ee b/apps/server/src/ee
index a5eb8d1e..4bac9b0a 160000
--- a/apps/server/src/ee
+++ b/apps/server/src/ee
@@ -1 +1 @@
-Subproject commit a5eb8d1e9a1bdb914321041929f88dd4296542bb
+Subproject commit 4bac9b0a3fe6c238351b0814d49cce7f5fa2e4af
diff --git a/apps/server/src/integrations/import/import.controller.ts b/apps/server/src/integrations/import/import.controller.ts
index 7ee325e5..69cb4937 100644
--- a/apps/server/src/integrations/import/import.controller.ts
+++ b/apps/server/src/integrations/import/import.controller.ts
@@ -51,7 +51,7 @@ export class ImportController {
@AuthUser() user: User,
@AuthWorkspace() workspace: Workspace,
) {
- const validFileExtensions = ['.md', '.html', '.docx'];
+ const validFileExtensions = ['.md', '.html', '.docx', '.pdf'];
const maxFileSize = bytes('20mb');
@@ -102,6 +102,7 @@ export class ImportController {
'.md': 'markdown',
'.html': 'html',
'.docx': 'docx',
+ '.pdf': 'pdf',
};
if (createdPage) {
diff --git a/apps/server/src/integrations/import/services/import.service.ts b/apps/server/src/integrations/import/services/import.service.ts
index 231a6c89..78cf0f23 100644
--- a/apps/server/src/integrations/import/services/import.service.ts
+++ b/apps/server/src/integrations/import/services/import.service.ts
@@ -62,7 +62,10 @@ export class ImportService {
let createdPage = null;
// For DOCX, we need the page ID upfront so images can reference it
- const pageId = fileExtension === '.docx' ? uuid7() : undefined;
+ const pageId =
+ fileExtension === '.docx' || fileExtension === '.pdf'
+ ? uuid7()
+ : undefined;
try {
if (fileExtension.endsWith('.md')) {
@@ -77,6 +80,14 @@ export class ImportService {
pageId,
userId,
);
+ } else if (fileExtension.endsWith('.pdf')) {
+ prosemirrorState = await this.processPdf(
+ fileBuffer,
+ workspaceId,
+ spaceId,
+ pageId,
+ userId,
+ );
}
} catch (err) {
const message = 'Error processing file content';
@@ -153,7 +164,7 @@ export class ImportService {
let DocxImportModule: any;
try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
- DocxImportModule = require('./../../../ee/docx-import/docx-import.service');
+ DocxImportModule = require('./../../../ee/document-import/docx-import.service');
} catch (err) {
this.logger.error(
'DOCX import requested but EE module not bundled in this build',
@@ -179,6 +190,42 @@ export class ImportService {
return this.processHTML(html);
}
+ async processPdf(
+ fileBuffer: Buffer,
+ workspaceId: string,
+ spaceId: string,
+ pageId: string,
+ userId: string,
+ ): Promise {
+ let PdfImportModule: any;
+ try {
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
+ PdfImportModule = require('./../../../ee/document-import/pdf-import.service');
+ } catch (err) {
+ this.logger.error(
+ 'PDF import requested but EE module not bundled in this build',
+ );
+ throw new BadRequestException(
+ 'This feature requires a valid enterprise license.',
+ );
+ }
+
+ const pdfImportService = this.moduleRef.get(
+ PdfImportModule.PdfImportService,
+ { strict: false },
+ );
+
+ const html = await pdfImportService.convertPdfToHtml(
+ fileBuffer,
+ workspaceId,
+ spaceId,
+ pageId,
+ userId,
+ );
+
+ return this.processHTML(html);
+ }
+
async createYdoc(prosemirrorJson: any): Promise {
if (prosemirrorJson) {
// this.logger.debug(`Converting prosemirror json state to ydoc`);