Initial working version

This commit is contained in:
Max Niclas Wächtler 2025-04-25 23:54:43 +02:00
parent 71c6b8e7ba
commit f1a36cde99
6 changed files with 2267 additions and 35 deletions

50
canvasFactory.ts Normal file
View file

@ -0,0 +1,50 @@
import { Canvas, createCanvas, CanvasRenderingContext2D } from "canvas";
type CanvasWithContext = {
canvas: Canvas | null;
context: CanvasRenderingContext2D | null;
};
export class NodeCanvasFactory {
create(
width: number,
height: number,
transparent: boolean
): CanvasWithContext {
const canvas = createCanvas(width, height);
const context = canvas.getContext("2d", { alpha: transparent });
if (transparent) context.clearRect(0, 0, width, height);
return {
canvas,
context,
};
}
reset(
canvasAndContext: CanvasWithContext,
width: number,
height: number
): void {
if (!canvasAndContext.canvas) {
throw new Error("Canvas is not specified");
}
if (!canvasAndContext.context) {
throw new Error("Context is not specified");
}
canvasAndContext.canvas.width = width;
canvasAndContext.canvas.height = height;
}
destroy(canvasAndContext: CanvasWithContext) {
if (!canvasAndContext.canvas) {
throw new Error("Canvas is not specified");
}
if (!canvasAndContext.context) {
throw new Error("Context is not specified");
}
canvasAndContext.canvas.width = 0;
canvasAndContext.canvas.height = 0;
canvasAndContext.canvas = null;
canvasAndContext.context = null;
}
}

120
main.ts
View file

@ -1,3 +1,4 @@
import { NodeCanvasFactory } from "canvasFactory";
import {
App,
Editor,
@ -8,9 +9,8 @@ import {
PluginSettingTab,
Setting,
TFile,
moment,
loadPdfJs,
} from "obsidian";
import { Options, pdf } from "pdf-to-img";
// Remember to rename these classes and interfaces!
@ -22,6 +22,16 @@ const DEFAULT_SETTINGS: MyPluginSettings = {
mySetting: "default",
};
type PDFDocumentBuffer = {
fileName: string;
pages: PDFPage[];
};
type PDFPage = {
pageNumber: number;
buffer: ArrayBuffer;
};
export default class MyPlugin extends Plugin {
settings: MyPluginSettings;
@ -63,9 +73,14 @@ export default class MyPlugin extends Plugin {
if (!pdfFile) {
return;
}
const pngFiles = await this.parsePDF(pdfFile, { scale: 2 });
const fileBufferArray = await this.parsePDF(pdfFile);
const imagePathList = await this.convertPDFBufferToImages({
fileName: pdfFile.name,
pages: fileBufferArray,
});
editor.replaceSelection(
pngFiles.map((f) => `![[${f}]]`).join("\n")
imagePathList.map((f) => `![[${f}]]`).join("\n")
);
},
});
@ -121,6 +136,12 @@ export default class MyPlugin extends Plugin {
await this.saveData(this.settings);
}
/**
* Checks if the input string adheres to the format ![[something]].
* @param input the string to parse
* @description checks if the input is a valid file path
* @returns found file path (something) or null
*/
private checkPathInput(input: string): string | null {
const matches = input.match(/!\[\[([^\]]+)\]\]/); // match ![[something]], -> something (capture group 1)
if (matches && matches[1]) {
@ -129,16 +150,14 @@ export default class MyPlugin extends Plugin {
return null;
}
/**
* Takes the path of a file and returns the TFile object if it exists in the vault.
* If the file is not found, it returns null.
* @param path path to the file (either just the name or the full path)
* @returns File object if it exists, null otherwise
*/
private fetchVaultFile(path: string): TFile | null {
const vaultFiles = this.app.vault.getFiles();
const file =
vaultFiles.find((f) => f.path === path) ??
vaultFiles.find((f) => f.path.contains(path)); // we attempt to find the file by path, if that fails we try to find it by substring
if (!file) {
new Notice(`File not found: ${path}.`);
return null;
}
return file;
return this.app.metadataCache.getFirstLinkpathDest(path, "");
}
/**
@ -171,19 +190,72 @@ export default class MyPlugin extends Plugin {
return file;
}
async parsePDF(file: TFile, options: Options): Promise<string[]> {
const document = await pdf(file.path, options);
let i = 1;
let fileNames: string[] = [];
for await (const image of document) {
const newFile = await this.app.vault.createBinary(
file.basename + `2img-${i}.png`,
image
/**
* Parses a given TFile object (PDF) and converts it to PNG images.
* The images are saved in the vault and their paths are returned as an array of strings.
* @param file The TFile object representing the PDF file to be parsed.
* @description The function uses the pdfjs library to convert the PDF to images.
* @throws Error if the canvas or context cannot be created.
* @returns An array of strings representing the paths of the saved PNG images.
*/
async parsePDF(file: TFile): Promise<PDFPage[]> {
const pdfjs = await loadPdfJs();
const arrayBuffer = await this.app.vault.adapter.readBinary(file.path);
const buffer = new Uint8Array(arrayBuffer);
const document = await pdfjs.getDocument(buffer).promise;
const pages: number = document.numPages;
let pdfBuffer: PDFPage[] = [];
for (let i = 1; i <= pages; i++) {
const page = await document.getPage(i);
const viewport = page.getViewport({ scale: 2 });
const canvasFactory = new NodeCanvasFactory();
const { canvas, context } = canvasFactory.create(
viewport.width,
viewport.height,
false
);
i++;
fileNames.push(newFile.path);
if (!canvas || !context) {
console.error("could not generate canvas or context");
return [];
}
await page.render({ canvasContext: context, viewport }).promise;
const blob = await new Promise<Blob | null>((resolve) => {
/** @ts-ignore because toBlob exists, but is not found */
canvas.toBlob(resolve, "image/png");
});
if (blob) {
pdfBuffer.push({
pageNumber: i,
buffer: await blob.arrayBuffer(), // Store base64 string (binary data)
});
} else {
console.error(`could not generate blob for page ${i}`);
}
}
return fileNames;
return pdfBuffer;
}
/**
* Converts a PDF page buffer to a PNG file and saves it in the vault.
* @param page The PDF page buffer to convert.
* @param pageNumber The page number of the PDF.
* @returns The path of the saved PNG file.
*/
private async convertPDFBufferToImages(
pdfBuffer: PDFDocumentBuffer
): Promise<string[]> {
const pngFilePaths: string[] = [];
for (const page of pdfBuffer.pages) {
const pngFileName = `${pdfBuffer.fileName}-${page.pageNumber}.png`;
const pngFilePath = `${pngFileName}`;
await this.app.vault.createBinary(pngFilePath, page.buffer);
pngFilePaths.push(pngFilePath);
}
return pngFilePaths;
}
}

View file

@ -1,11 +1,10 @@
{
"id": "pdf-annotator",
"name": "PDF Annotator",
"version": "0.0.1",
"id": "obsidian-pdf-printer",
"name": "PDF Printer",
"version": "0.1.0",
"minAppVersion": "0.15.0",
"description": "A tool that allows greater annotation of PDFs.",
"author": "Obsidian",
"description": "Convert your PDF notes/files to images without external dependencies.",
"author": "cubexy",
"authorUrl": "https://github.com/cubexy",
"fundingUrl": "https://www.tierschutzbund.de/helfen/spenden/jetzt-spenden/",
"isDesktopOnly": true
"isDesktopOnly": false
}

2111
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -1,7 +1,7 @@
{
"name": "obsidian-sample-plugin",
"version": "1.0.0",
"description": "This is a sample plugin for Obsidian (https://obsidian.md)",
"name": "obsidian-pdf-printer",
"version": "0.1.0",
"description": "Convert your PDF notes/files to images without external dependencies.",
"main": "main.js",
"scripts": {
"dev": "node esbuild.config.mjs",

View file

@ -1,3 +1,3 @@
{
"1.0.0": "0.15.0"
"0.1.0": "0.15.0"
}