Compare commits

...

4 commits

Author SHA1 Message Date
Nick
c7f531517a Update deps 2026-05-01 21:31:59 +02:00
Nick
6b6f37d903 Bump version to 1.4.1 2025-07-28 21:32:53 +02:00
Nick
d2dc1ef4cb Bumped version to 1.4.0 2025-07-27 10:42:25 +02:00
Benjamin Nelan
fcd8f9daf6
feat: Add support for Clip Studio Paint .clip files via sql.js and Web Assembly (#7)
* Added ClipStudioPaint support and necessary setting.
2025-07-27 10:39:43 +02:00
10 changed files with 2919 additions and 297 deletions

View file

@ -21,6 +21,7 @@ The file types that are currently supported are:
- `.kra` (Files made in Krita)
- `.psd` (Photoshop, other visual programs)
- `.ai` (Adobe Illustrator)
- `.clip` (Clip Studio Paint)
- `.gltf`, `.glb` (3D scene format)
- `.obj` (3D object format)
- `.stl` (3D format, often used for 3d printing)

View file

@ -1,6 +1,8 @@
import esbuild from "esbuild";
import process from "process";
import builtins from "builtin-modules";
import { readFileSync } from "fs";
import { join } from 'path';
const banner =
`/*
@ -11,6 +13,33 @@ if you want to view the source, please visit the github repository of this plugi
const prod = (process.argv[2] === "production");
const wasmPlugin = {
name: 'wasm',
setup(build) {
build.onResolve({ filter: /\.wasm$/ }, args => {
if (args.resolveDir === '') return;
return {
path: join(args.resolveDir, args.path),
namespace: 'wasm-binary',
};
});
build.onLoad({ filter: /\.wasm$/, namespace: 'wasm-binary' }, async (args) => {
const contents = readFileSync(args.path);
const wasmBase64 = contents.toString('base64');
return {
contents: `
const wasmBase64 = "${wasmBase64}";
const wasmBinary = Uint8Array.from(atob(wasmBase64), c => c.charCodeAt(0));
export default wasmBinary;
`,
loader: 'js',
};
});
},
};
const context = await esbuild.context({
banner: {
js: banner,
@ -38,6 +67,9 @@ const context = await esbuild.context({
sourcemap: prod ? false : "inline",
treeShaking: true,
outfile: "main.js",
plugins: [
wasmPlugin
],
minify: prod,
});

11
main.ts
View file

@ -97,6 +97,17 @@ class ExtendedFileSupportSettingTab extends PluginSettingTab {
this.plugin.toggleExtension("psd", value);
}));
new Setting(containerEl)
.setName(".clip")
.setDesc("Files generated by Clip Studio Paint.")
.addToggle(toggle => toggle
.setValue(this.plugin.settings.clip)
.onChange(async (value) => {
this.plugin.settings.clip = value;
await this.plugin.saveSettings();
this.plugin.toggleExtension("clip", value);
}));
new Setting(containerEl)
.setName(".kra")
.setDesc("Files generated by Krita.")

View file

@ -1,7 +1,7 @@
{
"id": "extended-file-support",
"name": "Extended File Support",
"version": "1.3.0",
"version": "1.4.1",
"minAppVersion": "0.18.0",
"description": "Adds file support for various file types. Allows viewing and embedding these filetypes. Includes: .kra, .psd, .obj, .glb, .gltf, and more.",
"author": "Nick de Bruin",

3044
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
{
"name": "extended-file-support",
"version": "1.3.0",
"version": "1.4.1",
"description": "Extended file support for Obsidian",
"main": "main.js",
"scripts": {
@ -14,6 +14,7 @@
"devDependencies": {
"@types/node": "^16.11.6",
"@types/pdfjs-dist": "^2.10.378",
"@types/sql.js": "^1.4.9",
"@types/three": "^0.172.0",
"@typescript-eslint/eslint-plugin": "5.29.0",
"@typescript-eslint/parser": "5.29.0",
@ -27,6 +28,7 @@
"dependencies": {
"@webtoon/psd": "^0.4.0",
"@zip.js/zip.js": "^2.6.40",
"sql.js": "^1.13.0",
"three": "^0.172.0"
}
}

114
src/extensions/clip.ts Normal file
View file

@ -0,0 +1,114 @@
import { AltTextParsed, ExtensionComponent } from "src/extensionComponent";
import { ExtensionView } from "src/extensionView";
import initSqlJs from 'sql.js'
// @ts-ignore
import wasmBinary from '../../node_modules/sql.js/dist/sql-wasm.wasm';
export const VIEW_TYPE_CLIP = "extended-file-support-clip";
export class CLIPComponent extends ExtensionComponent {
private sql: any = null
private objectURL?: string;
parseLinkText(_: AltTextParsed): void { }
private async getSql() {
if (!this.sql) {
try{
this.sql = await initSqlJs({
wasmBinary: wasmBinary
});
} catch (error) {
console.error('Failed to load sql.js WebAssembly file required to read .clip files.', error);
this.sql = false;
}
}
return this.sql;
}
async loadFile(): Promise<void> {
const resource = this.plugin.app.vault.getResourcePath(this.file);
const res = await fetch(resource);
const arrayBuffer = await res.arrayBuffer();
const offset = this.getDatabaseOffset(arrayBuffer);
if (offset !== -1) {
const sqliteDB = arrayBuffer.slice(offset);
const SQL = await this.getSql();
try{
const db = new SQL.Database(new Uint8Array(sqliteDB));
const result = db.exec("SELECT imageData FROM CanvasPreview");
if ( !result || result.length === 0 ) {
throw new Error("No image data found in CanvasPreview table");
}
const imageBytes = result[0].values[0][0] as Uint8Array;
const image_file = new Blob([imageBytes]);
this.objectURL = URL.createObjectURL(image_file);
} catch (error) {
console.error('Error accessing imageData from sqlite database.', error);
}
}
if (this.objectURL) {
const image = new Image();
image.src = this.objectURL;
image.alt = this.file.name;
if (this.width) {
image.width = this.width;
}
if (this.height) {
image.height = this.height;
}
this.contentEl.empty();
this.contentEl.removeClass("extended-file-loading");
this.contentEl.addClasses(["media-embed", "image-embed"]);
this.contentEl.append(image);
} else {
this.contentEl.empty();
this.contentEl.createEl("i", { text: `Could not load ${this.file.path}` });
}
}
cleanup(): void {
if (this.objectURL) {
URL.revokeObjectURL(this.objectURL);
this.objectURL = undefined;
}
}
getDatabaseOffset(arrayBuffer: ArrayBuffer): number {
const haystack = new Uint8Array(arrayBuffer);
const needle = new TextEncoder().encode('SQLite format 3\0');
for (let i = 0; i <= haystack.length - needle.length; i++) {
let found = true;
for (let j = 0; j < needle.length; j++) {
if (haystack[i + j] !== needle[j]) {
found = false;
break;
}
}
if (found) {
return i;
}
}
return -1;
}
}
export class CLIPView extends ExtensionView<CLIPComponent> {
getIcon(): string {
return "image";
}
getComponent(): new (...args: ConstructorParameters<typeof ExtensionComponent>) => CLIPComponent {
return CLIPComponent;
}
getViewType(): string {
return VIEW_TYPE_CLIP;
}
}

View file

@ -4,6 +4,7 @@ import { ExtensionComponent } from "./extensionComponent"
import { OBJComponent, OBJView, VIEW_TYPE_OBJ } from "./extensions/obj"
import { GLBComponent, GLBView, VIEW_TYPE_GLB } from "./extensions/glb"
import { PSDComponent, PSDView, VIEW_TYPE_PSD } from "./extensions/psd"
import { CLIPComponent, CLIPView, VIEW_TYPE_CLIP } from "./extensions/clip"
import { STLComponent, STLView, VIEW_TYPE_STL } from "./extensions/stl"
import { AIComponent, AIView, VIEW_TYPE_AI } from "./extensions/ai"
@ -17,6 +18,7 @@ export type Extension = {
// Type should match settings field
export const EXTENSION_REGISTRY: Extension[] = [
{ types: ["kra"], view_type: VIEW_TYPE_KRA, view: KRAView, component: KRAComponent },
{ types: ["clip"], view_type: VIEW_TYPE_CLIP, view: CLIPView, component: CLIPComponent },
{ types: ["obj"], view_type: VIEW_TYPE_OBJ, view: OBJView, component: OBJComponent },
{ types: ["glb", "gltf"], view_type: VIEW_TYPE_GLB, view: GLBView, component: GLBComponent },
{ types: ["psd"], view_type: VIEW_TYPE_PSD, view: PSDView, component: PSDComponent },

View file

@ -1,5 +1,6 @@
export interface ExtendedFileSupportSettings {
kra: boolean;
clip: boolean;
psd: boolean;
ai: boolean;
ai_render_scale: number;
@ -14,6 +15,7 @@ export interface ExtendedFileSupportSettings {
export const DEFAULT_SETTINGS: ExtendedFileSupportSettings = {
kra: true,
clip: true,
psd: true,
ai: true,
ai_render_scale: 1.5,

View file

@ -4,5 +4,7 @@
"1.2.1": "0.18.0",
"1.2.4": "0.18.0",
"1.2.5": "0.18.0",
"1.3.0": "0.18.0"
"1.3.0": "0.18.0",
"1.4.0": "0.18.0",
"1.4.1": "0.18.0"
}