diff --git a/apps/client/src/components/note_context.ts b/apps/client/src/components/note_context.ts
index b78cca3b3..f897e0673 100644
--- a/apps/client/src/components/note_context.ts
+++ b/apps/client/src/components/note_context.ts
@@ -35,6 +35,10 @@ export interface NoteContextDataMap {
attachments: Array<{ filename: string; size: number }>;
downloadAttachment(filename: string): void;
};
+ pdfLayers: {
+ layers: Array<{ id: string; name: string; visible: boolean }>;
+ toggleLayer(layerId: string, visible: boolean): void;
+ };
}
type ContextDataKey = keyof NoteContextDataMap;
diff --git a/apps/client/src/widgets/sidebar/PdfLayers.css b/apps/client/src/widgets/sidebar/PdfLayers.css
new file mode 100644
index 000000000..cde66f218
--- /dev/null
+++ b/apps/client/src/widgets/sidebar/PdfLayers.css
@@ -0,0 +1,54 @@
+.pdf-layers-list {
+ width: 100%;
+}
+
+.pdf-layer-item {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 12px;
+ cursor: pointer;
+ border-bottom: 1px solid var(--main-border-color);
+ transition: background-color 0.2s;
+}
+
+.pdf-layer-item:hover {
+ background-color: var(--hover-item-background-color);
+}
+
+.pdf-layer-item:last-child {
+ border-bottom: none;
+}
+
+.pdf-layer-item.hidden {
+ opacity: 0.5;
+}
+
+.pdf-layer-name {
+ flex: 1;
+ font-size: 13px;
+ color: var(--main-text-color);
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.no-layers {
+ padding: 16px;
+ text-align: center;
+ color: var(--muted-text-color);
+}
+
+.pdf-layer-item .bx {
+ flex-shrink: 0;
+ font-size: 18px;
+ color: var(--muted-text-color);
+}
+
+.pdf-layer-item:hover .bx {
+ color: var(--main-text-color);
+}
+
+.pdf-layer-item.visible .bx {
+ color: var(--main-text-color);
+}
diff --git a/apps/client/src/widgets/sidebar/PdfLayers.tsx b/apps/client/src/widgets/sidebar/PdfLayers.tsx
new file mode 100644
index 000000000..d950783c3
--- /dev/null
+++ b/apps/client/src/widgets/sidebar/PdfLayers.tsx
@@ -0,0 +1,65 @@
+import "./PdfLayers.css";
+
+import { useActiveNoteContext, useGetContextData, useNoteProperty } from "../react/hooks";
+import Icon from "../react/Icon";
+import RightPanelWidget from "./RightPanelWidget";
+
+interface LayerInfo {
+ id: string;
+ name: string;
+ visible: boolean;
+}
+
+export default function PdfLayers() {
+ const { note } = useActiveNoteContext();
+ const noteType = useNoteProperty(note, "type");
+ const noteMime = useNoteProperty(note, "mime");
+
+ if (noteType !== "file" || noteMime !== "application/pdf") {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+}
+
+function PdfLayersList() {
+ const layersData = useGetContextData("pdfLayers");
+
+ if (!layersData || layersData.layers.length === 0) {
+ return
No layers
;
+ }
+
+ return (
+
+ {layersData.layers.map((layer) => (
+
+ ))}
+
+ );
+}
+
+function PdfLayerItem({
+ layer,
+ onToggle
+}: {
+ layer: LayerInfo;
+ onToggle: (layerId: string, visible: boolean) => void;
+}) {
+ return (
+ onToggle(layer.id, !layer.visible)}
+ >
+
+
{layer.name}
+
+ );
+}
diff --git a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx
index 8144972cb..465631682 100644
--- a/apps/client/src/widgets/sidebar/RightPanelContainer.tsx
+++ b/apps/client/src/widgets/sidebar/RightPanelContainer.tsx
@@ -16,6 +16,7 @@ import Icon from "../react/Icon";
import LegacyRightPanelWidget from "../right_panel_widget";
import HighlightsList from "./HighlightsList";
import PdfAttachments from "./PdfAttachments";
+import PdfLayers from "./PdfLayers";
import PdfPages from "./PdfPages";
import RightPanelWidget from "./RightPanelWidget";
import TableOfContents from "./TableOfContents";
@@ -77,6 +78,10 @@ function useItems(rightPaneVisible: boolean, widgetsByParent: WidgetsByParent) {
el: ,
enabled: isPdf,
},
+ {
+ el: ,
+ enabled: isPdf,
+ },
{
el: ,
enabled: noteType === "text" && highlightsList.length > 0,
diff --git a/apps/client/src/widgets/type_widgets/file/Pdf.tsx b/apps/client/src/widgets/type_widgets/file/Pdf.tsx
index bac8e3cf1..638d5fb30 100644
--- a/apps/client/src/widgets/type_widgets/file/Pdf.tsx
+++ b/apps/client/src/widgets/type_widgets/file/Pdf.tsx
@@ -121,6 +121,19 @@ export default function PdfPreview({ note, blob, componentId, noteContext }: {
}
});
}
+
+ if (event.data.type === "pdfjs-viewer-layers") {
+ noteContext.setContextData("pdfLayers", {
+ layers: event.data.layers,
+ toggleLayer: (layerId: string, visible: boolean) => {
+ iframeRef.current?.contentWindow?.postMessage({
+ type: "trilium-toggle-layer",
+ layerId,
+ visible
+ }, "*");
+ }
+ });
+ }
}
window.addEventListener("message", handleMessage);
diff --git a/packages/pdfjs-viewer/src/custom.ts b/packages/pdfjs-viewer/src/custom.ts
index 917cf5958..8dec7bcaf 100644
--- a/packages/pdfjs-viewer/src/custom.ts
+++ b/packages/pdfjs-viewer/src/custom.ts
@@ -2,6 +2,7 @@ import interceptPersistence from "./persistence";
import { extractAndSendToc, setupScrollToHeading, setupActiveHeadingTracking } from "./toc";
import { setupPdfPages } from "./pages";
import { setupPdfAttachments } from "./attachments";
+import { setupPdfLayers } from "./layers";
const LOG_EVENT_BUS = false;
@@ -25,6 +26,7 @@ async function main() {
setupActiveHeadingTracking();
setupPdfPages();
setupPdfAttachments();
+ setupPdfLayers();
});
await app.initializedPromise;
};
diff --git a/packages/pdfjs-viewer/src/layers.ts b/packages/pdfjs-viewer/src/layers.ts
new file mode 100644
index 000000000..b21a3b653
--- /dev/null
+++ b/packages/pdfjs-viewer/src/layers.ts
@@ -0,0 +1,100 @@
+export async function setupPdfLayers() {
+ const app = window.PDFViewerApplication;
+
+ // Extract immediately since we're called after documentloaded
+ await extractAndSendLayers();
+
+ // Listen for layer visibility toggle requests
+ window.addEventListener("message", async (event) => {
+ if (event.data?.type === "trilium-toggle-layer") {
+ const layerId = event.data.layerId;
+ const visible = event.data.visible;
+ await toggleLayer(layerId, visible);
+ }
+ });
+}
+
+async function extractAndSendLayers() {
+ const app = window.PDFViewerApplication;
+
+ try {
+ const optionalContentConfig = await app.pdfDocument.getOptionalContentConfig();
+
+ if (!optionalContentConfig) {
+ window.parent.postMessage({
+ type: "pdfjs-viewer-layers",
+ layers: []
+ }, "*");
+ return;
+ }
+
+ // Get all layer group IDs from the order
+ const order = optionalContentConfig.getOrder();
+ if (!order || order.length === 0) {
+ window.parent.postMessage({
+ type: "pdfjs-viewer-layers",
+ layers: []
+ }, "*");
+ return;
+ }
+
+ // Flatten the order array (it can be nested) and extract group IDs
+ const groupIds: string[] = [];
+ const flattenOrder = (items: any[]) => {
+ for (const item of items) {
+ if (typeof item === 'string') {
+ groupIds.push(item);
+ } else if (Array.isArray(item)) {
+ flattenOrder(item);
+ } else if (item && typeof item === 'object' && item.id) {
+ groupIds.push(item.id);
+ }
+ }
+ };
+ flattenOrder(order);
+
+ // Get group details for each ID
+ const layers = groupIds.map(id => {
+ const group = optionalContentConfig.getGroup(id);
+ return {
+ id,
+ name: group?.name || `Layer ${id}`,
+ visible: optionalContentConfig.isVisible(id)
+ };
+ }).filter(layer => layer.name); // Filter out invalid layers
+
+ window.parent.postMessage({
+ type: "pdfjs-viewer-layers",
+ layers
+ }, "*");
+ } catch (error) {
+ console.error("Error extracting layers:", error);
+ window.parent.postMessage({
+ type: "pdfjs-viewer-layers",
+ layers: []
+ }, "*");
+ }
+}
+
+async function toggleLayer(layerId: string, visible: boolean) {
+ const app = window.PDFViewerApplication;
+
+ try {
+ const optionalContentConfig = await app.pdfDocument.getOptionalContentConfig();
+
+ if (!optionalContentConfig) {
+ return;
+ }
+
+ // Set visibility
+ optionalContentConfig.setVisibility(layerId, visible);
+
+ // Trigger re-render
+ app.eventBus.dispatch("optionalcontentconfigchanged");
+
+ // Send updated layer state back
+ await extractAndSendLayers();
+ } catch (error) {
+ console.error("Error toggling layer:", error);
+ }
+}