Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

feat: Add PDF import functionality and DPI menu#187

Open
fnzcmd wants to merge 1 commit into
tldraw:maintldraw/obsidian-plugin:mainfrom
fnzcmd:mainfnzcmd/tldraw-in-obsidian:mainCopy head branch name to clipboard
Open

feat: Add PDF import functionality and DPI menu#187
fnzcmd wants to merge 1 commit into
tldraw:maintldraw/obsidian-plugin:mainfrom
fnzcmd:mainfnzcmd/tldraw-in-obsidian:mainCopy head branch name to clipboard

Conversation

@fnzcmd

@fnzcmd fnzcmd commented Jan 8, 2026

Copy link
Copy Markdown

feat: Add PDF import functionality with page selection and DPI settings

This PR adds comprehensive PDF import functionality to the tldraw-in-obsidian plugin, allowing users to import PDF files directly into their tldraw canvases with full control over page selection and rendering quality.

Features

PDF Import Modal

  • Intuitive page selection: Select individual pages or use range notation (e.g., 1-3,5,7-10)
  • Quick select buttons: "All" and "None" for batch selection
  • Page dimensions display: Shows width × height for each page
  • Configurable options:
    • Group pages: Optionally group all imported pages together
    • Spacing: Customize horizontal spacing between PDFs and existing shapes
    • DPI setting: Adjustable render quality (72-300 DPI, default: 150)

PDF Rendering

  • Custom pdf-page shape type: New tldraw shape for native PDF page rendering
  • PdfPageShapeUtil: Custom shape utility with proper resize handling
  • PdfPageRenderer: React component for rendering PDF pages using PDF.js
  • Lazy loading: PDF.js worker is loaded efficiently via blob URL

PDF Editor Component

  • Full-featured PDF annotation editor based on tldraw's PDF example
  • Locked page backgrounds: PDF pages stay locked and at the bottom of z-order
  • Page overlay: Visual dimming outside PDF page bounds
  • Responsive camera constraints: Adapts to mobile and desktop viewports
  • Export functionality: Export annotated PDFs

DPI Quality Menu

  • Integrated into tldraw's File menu as "Change PDF Quality (DPI)"
  • Custom dialog (replaces prompt() which doesn't work in Electron/Obsidian)
  • Theme-aware styling: Dialog matches Obsidian's current theme (dark/light mode)
  • Works with both individual PDF shapes and groups containing PDFs

Technical Changes

New Files

File Purpose
src/components/pdf/index.ts PDF module exports
src/components/pdf/PdfEditor.tsx Main PDF editor component
src/components/pdf/PdfPageRenderer.tsx PDF page rendering with PDF.js
src/components/pdf/PdfPicker.ts PDF file picker utility
src/components/pdf/loadPdf.ts PDF loading and parsing
src/components/pdf/pdf.types.ts TypeScript types for PDF handling
src/components/pdf/ExportPdfButton.tsx Export button component
src/obsidian/modal/PdfImportModal.ts Obsidian modal for import options
src/tldraw/shapes/PdfPageShapeUtil.tsx Custom tldraw shape for PDF pages
src/tldraw/shapes/index.ts Shape exports
src/utils/migration.ts Migration utilities

Modified Files

  • esbuild.config.mjs - Added text loader for PDF.js worker
  • package.json / package-lock.json - Added pdfjs-dist dependency
  • src/components/TldrawApp.tsx - Integrated PDF components
  • src/tldraw/ui-overrides.ts - Added DPI menu option
  • src/styles.css - Added PDF modal and overlay styles
  • Various context and hook files for PDF integration

Container Support

Added containerization files for development:

  • Containerfile
  • compose.yaml
  • .containerignore

Screenshots

image image image

Testing

  • PDF import with single page
  • PDF import with multiple pages
  • Page range selection
  • DPI quality adjustment
  • Group pages option
  • Dark/light theme compatibility

Breaking Changes

None

@AE-SAY-WAY

Copy link
Copy Markdown

@fnzcmd excellent work bro! @jon-dez what do you think of this?

@jon-dez

jon-dez commented Jan 23, 2026

Copy link
Copy Markdown
Collaborator

@fnzcmd Hey this is pretty cool! I'll need some time to look this over in full, but in the meantime do you think it would be possible to do a bit of refactoring here? I think moving all functionality that relates to the PDF functionality into its own folder would be ideal. Maybe under root folder in extensions/pdf/ so the structure kind of mirrors the src/ folder:

src/

  • components/
  • obsidian
  • tldraw

extensions/pdf/

  • components/
  • obsidian/
  • tldraw/

I think minimizing the amount of files modified would be ideal. If a file was modified try to limit it to importing from files under the pdf directory. I'll add some comments where I notice refactoring could be done.

@jon-dez jon-dez left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@fnzcmd Overall I think this would be a great addition. I do recommend you make the changes regarding refactoring the pdf related features into its own folder so that way it keeps it modularized and easier to navigate the files related to pdf functionality.

Comment thread esbuild.config.mjs
Comment on lines +21 to +34
// Plugin to load pdf.worker.min.mjs as text for blob URL creation
const pdfWorkerPlugin = {
name: 'pdf-worker-text',
setup(build) {
build.onLoad({ filter: /pdf\.worker\.min\.mjs$/ }, async (args) => {
const contents = readFileSync(args.path, 'utf8');
return {
contents: `export default ${JSON.stringify(contents)};`,
loader: 'js',
};
});
},
};

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe refactor into extensions/pdf/esbuild-plugin.mjs?

export default {
	name: 'pdf-worker-blob',
	setup(build) {
		build.onLoad({ filter: /pdf\.worker\.min\.mjs$/ }, async (args) => {
			const contents = readFileSync(args.path, 'utf8');
			return {
				contents: 'export default ${JSON.stringify(contents)};`,
				loader: 'js',
			};
		});
	},
}

Also, I see that the worker code is exported as a string in contents, then made into a blob later on in different places. Perhaps it could be done directly like this?

const pdfWorkerCode = ${JSON.stringify(contents)}
const pdfWorkerBlob = new Blob([pdfWorkerCode], { type: 'application/javascript' })
export default pdfWorkerBlob

This avoid creating multiple blob objects, reusing the one created in this module.

Comment on lines +112 to +161
// Custom context menu with PDF DPI option
function CustomContextMenu(props: any) {
const editor = useEditor();
const actions = useActions();

// Helper to recursively get all descendant shapes
function getAllDescendants(shapes: any[]): any[] {
const result: any[] = [];
for (const shape of shapes) {
result.push(shape);
if (shape.type === 'group') {
const children = editor.getSortedChildIdsForParent(shape.id)
.map((id: any) => editor.getShape(id))
.filter(Boolean);
result.push(...getAllDescendants(children as any[]));
}
}
return result;
}

// Check if any selected shape is a PDF
const selectedShapes = editor.getSelectedShapes();
const allShapes = getAllDescendants(selectedShapes);
const hasPdfSelected = allShapes.some((shape: any) => {
if (shape.type !== 'image') return false;
const asset = editor.getAsset((shape.props as any).assetId);
return asset && (asset.meta as any)?.isPdfAsset;
});

const handleChangePdfDpi = React.useCallback(() => {
actions['change-pdf-dpi']?.onSelect('context-menu' as any);
}, [actions]);

return (
<DefaultContextMenu {...props}>
{hasPdfSelected && (
<TldrawUiMenuGroup id="pdf-options">
<TldrawUiMenuItem
id="change-pdf-dpi"
label="Change PDF Quality (DPI)"
onSelect={handleChangePdfDpi}
/>
</TldrawUiMenuGroup>
)}
<DefaultContextMenuContent />
</DefaultContextMenu>
);
}


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Refactor custom context menu items like so

src/components/TldrawApp.tsx

import { PdfContextMenuItems } from "extensions/pdf/context-menu-items"

function CustomContextMenu(props: any) {
	return (
		<DefaultContextMenu {...props}>
			<PdfContextMenuItems />
			<DefaultContextMenuContent />
		</DefaultContextMenu>
	);
}

extensions/pdf/components/context-menu-items.ts

// Custom menu items with PDF DPI option
export function PdfContextMenuItems() {
	const editor = useEditor();
	const actions = useActions();

	// Helper to recursively get all descendant shapes
	function getAllDescendants(shapes: any[]): any[] {
		const result: any[] = [];
		for (const shape of shapes) {
			result.push(shape);
			if (shape.type === 'group') {
				const children = editor.getSortedChildIdsForParent(shape.id)
					.map((id: any) => editor.getShape(id))
					.filter(Boolean);
				result.push(...getAllDescendants(children as any[]));
			}
		}
		return result;
	}

	// Check if any selected shape is a PDF
	const selectedShapes = editor.getSelectedShapes();
	const allShapes = getAllDescendants(selectedShapes);
	const hasPdfSelected = allShapes.some((shape: any) => {
		if (shape.type !== 'image') return false;
		const asset = editor.getAsset((shape.props as any).assetId);
		return asset && (asset.meta as any)?.isPdfAsset;
	});

	const handleChangePdfDpi = React.useCallback(() => {
		actions['change-pdf-dpi']?.onSelect('context-menu' as any);
	}, [actions]);

	return (
		<>
			{hasPdfSelected && (
				<TldrawUiMenuGroup id="pdf-options">
					<TldrawUiMenuItem
						id="change-pdf-dpi"
						label="Change PDF Quality (DPI)"
						onSelect={handleChangePdfDpi}
					/>
				</TldrawUiMenuGroup>
			)}
		</>
	);
}



function LocalFileMenu(props: { plugin: TldrawPlugin }) {
const actions = useActions();

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar pattern as the context menu

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move into extensions/pdf/components/ folder

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor into extensions/pdf/components/ folder

Comment thread src/tldraw/asset-store.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor pdf related logic to extensions/pdf/tldraw/ ?

Comment on lines 191 to +196
const store = createTLStore({
snapshot: snapshot,
shapeUtils: defaultShapeUtils,
});

store.put(storeGroup.main.store.allRecords());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason this was changed?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor pdf related stuff to extensions/pdf/tldraw/

Comment thread src/utils/migration.ts

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

move to extensions/pdf/utils/

Comment thread src/styles.css

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor pdf related stuff to extensions/pdf/styles.css and import it into this file

@Nikita-Egunov

Nikita-Egunov commented Apr 9, 2026

Copy link
Copy Markdown

@jon-dez

Are there any plans to officially add PDF support to the plugin?
The feature in PR #187 is in high demand. If you need help with refinement or testing, I'm happy to help.

@jon-dez

jon-dez commented Apr 15, 2026

Copy link
Copy Markdown
Collaborator

@Nikita-Egunov Currently I have no plans to introduce PDF functionality as part of the core plugin features. I think PDF functionality should be implemented as some separate extension on top of this plugin, though an extensions API that allows users (or other plugins) to access tldraw components and hook into the editor does not exist yet.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants

Morty Proxy This is a proxified and sanitized view of the page, visit original site.