mirror of
https://github.com/mii443/obsidian-typst.git
synced 2025-08-22 16:15:34 +00:00
0.5.0 (#8)
Co-authored-by: paulaingate <49568567+paulaingate@users.noreply.github.com>
This commit is contained in:
@ -1,2 +0,0 @@
|
|||||||
[build]
|
|
||||||
rustflags = ["--cfg=web_sys_unstable_apis"]
|
|
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: fenjalien
|
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# 0.5.0
|
||||||
|
- Update Typst version to 0.6.0
|
||||||
|
- Attempt to fix bad canvas output
|
||||||
|
- Move compiler to a web worker
|
||||||
|
- Rework file reading
|
||||||
|
- Add support of reading packages outside of the vault
|
||||||
|
- Improve settings appearance (#7)
|
||||||
|
- Various bug fixes
|
627
Cargo.lock
generated
627
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
30
Cargo.toml
30
Cargo.toml
@ -1,23 +1,21 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "obsidian-typst"
|
name = "obsidian-typst"
|
||||||
version = "0.4.2"
|
version = "0.5.0"
|
||||||
authors = ["fenjalien"]
|
authors = ["fenjalien"]
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
description = "Renders `typst` code blocks to images with Typst."
|
description = "Renders `typst` code blocks to images with Typst."
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
rust-version = "1.70"
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
crate-type = ["cdylib"]
|
crate-type = ["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# Everything to do with Typst
|
# Everything to do with Typst
|
||||||
typst = { git = "https://github.com/typst/typst.git", tag = "v0.4.0" }
|
typst = { git = "https://github.com/typst/typst.git", tag = "v0.6.0" }
|
||||||
typst-library = { git = "https://github.com/typst/typst.git", tag = "v0.4.0" }
|
typst-library = { git = "https://github.com/typst/typst.git", tag = "v0.6.0" }
|
||||||
comemo = "0.3"
|
comemo = "0.3"
|
||||||
|
|
||||||
once_cell = "1.17.1"
|
|
||||||
siphasher = "0.3.10"
|
siphasher = "0.3.10"
|
||||||
elsa = "1.8.0"
|
|
||||||
|
|
||||||
|
|
||||||
# Everything to do with wasm
|
# Everything to do with wasm
|
||||||
@ -26,14 +24,14 @@ js-sys = "^0.3"
|
|||||||
wasm-bindgen-futures = "^0.4"
|
wasm-bindgen-futures = "^0.4"
|
||||||
serde = { version = "^1.0", features = ["derive"] }
|
serde = { version = "^1.0", features = ["derive"] }
|
||||||
serde-wasm-bindgen = "^0.5"
|
serde-wasm-bindgen = "^0.5"
|
||||||
web-sys = { version = "^0.3", features = ["console", "Window", "FontData", "Blob", "ImageData"] }
|
web-sys = { version = "^0.3", features = [
|
||||||
|
"console",
|
||||||
|
"Window",
|
||||||
|
"FontData",
|
||||||
|
"Blob",
|
||||||
|
"ImageData",
|
||||||
|
] }
|
||||||
|
console_error_panic_hook = "0.1.7"
|
||||||
|
|
||||||
|
# Image handling
|
||||||
# [patch.crates-io]
|
fast_image_resize = "2.7.3"
|
||||||
# web-sys = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
|
||||||
# js-sys = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
|
||||||
# wasm-bindgen-futures = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
|
||||||
# wasm-bindgen = { git = "https://github.com/fenjalien/wasm-bindgen.git" }
|
|
||||||
|
|
||||||
# [profile.release]
|
|
||||||
# debug = true
|
|
||||||
|
27
README.md
27
README.md
@ -2,15 +2,20 @@
|
|||||||
|
|
||||||
Renders `typst` code blocks, and optionally math blocks, into images using [Typst](https://github.com/typst/typst) through the power of WASM! This is still very much in development, so suggestions and bugs are welcome!
|
Renders `typst` code blocks, and optionally math blocks, into images using [Typst](https://github.com/typst/typst) through the power of WASM! This is still very much in development, so suggestions and bugs are welcome!
|
||||||
|
|
||||||
## Things to NOTE
|
## Small Things to NOTE
|
||||||
|
- This plugin uses Typst 0.6.0
|
||||||
- Typst does not currently support exporting to HTML only PDFs and PNGs. So due to image scaling, the rendered views may look a bit terrible. If you know how to fix this PLEASE HELP.
|
- Typst does not currently support exporting to HTML only PDFs and PNGs. So due to image scaling, the rendered views may look a bit terrible. If you know how to fix this PLEASE HELP.
|
||||||
- File paths should be relative to the vault folder.
|
- File paths should be relative to the vault folder.
|
||||||
- System fonts are not loaded by default as this takes about 20 seconds (on my machine). There is an option in settings to enable them (requires a reload of the plugin).
|
- System fonts are not loaded by default as this takes about 20 seconds (on my machine). There is an option in settings to enable them (requires a reload of the plugin).
|
||||||
|
- You can not import on mobile as file reading is NOT supported on mobile, this is due to `SharedArrayBuffer`s not being available on mobile but is available for some reason on desktop.
|
||||||
|
|
||||||
|
## Using Packages
|
||||||
|
The plugin supports only the reading of packages from the [`@preview`](https://github.com/typst/packages#downloads) and [`@local`](https://github.com/typst/packages#local-packages) namespaces. Please use the Typst cli to download your desired packages. This means the plugin accesses files outside of your vault but only to read them, it does not modify or create files outside of your vault.
|
||||||
|
|
||||||
## Math Block Usage
|
## Math Block Usage
|
||||||
The plugin can render `typst` inside math blocks! By default this is off, to enable it set the "Override Math Blocks" setting or use the "Toggle Math Block Override" command. Math block types are conserved between Obsidian and Typst, `$...$` -> `$...$` and `$$...$$` -> `$ ... $`.
|
The plugin can render `typst` inside math blocks! By default this is off, to enable it set the "Override Math Blocks" setting or use the "Toggle math block override" command. Math block types are conserved between Obsidian and Typst, `$...$` -> `$...$` and `$$...$$` -> `$ ... $`.
|
||||||
|
|
||||||
From what I've experimented with, normal math blocks are okay with `typst` code but Typst is not happy with any Latex code.
|
From what I've experimented with, normal math blocks are okay with Typst but Typst is not happy with any Latex code.
|
||||||
|
|
||||||
For styling and using imports with math blocks see the next section.
|
For styling and using imports with math blocks see the next section.
|
||||||
|
|
||||||
@ -23,10 +28,8 @@ Preamables are prepended to your `typst` code before compiling. There are three
|
|||||||
- `code`: Prepended to `typst` code only in code blocks.
|
- `code`: Prepended to `typst` code only in code blocks.
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
### "File Not Found" Error on First Load of Obsidian
|
### Runtime Error Unreachable or Recursive Use Of Object
|
||||||
When Obsidian first loads it sometimes tries to render before its files are resolved and cached.
|
These occur when the Typst compiler panics for any reason and means the compiler cannot be used again until it is restarted. There should be more information in the console log so please create an issue with this error!
|
||||||
|
|
||||||
To fix, simply select then deselect everything in the file, or close and re-open the file.
|
|
||||||
|
|
||||||
## Example
|
## Example
|
||||||
|
|
||||||
@ -61,14 +64,20 @@ The first #count numbers of the sequence are:
|
|||||||
<img src="assets/example.png">
|
<img src="assets/example.png">
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
Until this plugin is submitted to the community plugins please install it by copying `main.js`, `styles.css`, and `manifest.json` from the releases tab to the folder `.obsidian/plugins/obsidian-typst` in your vault.
|
Install "Typst Renderer" from the community plugins tab in settings
|
||||||
|
|
||||||
|
or
|
||||||
|
|
||||||
|
Install it by copying `main.js`, `styles.css`, and `manifest.json` from the releases tab to the folder `.obsidian/plugins/obsidian-typst` in your vault.
|
||||||
|
|
||||||
## TODO / Goals (In no particular order)
|
## TODO / Goals (In no particular order)
|
||||||
- [x] Better font loading
|
- [x] Better font loading
|
||||||
- [x] Fix importing
|
- [x] Fix importing
|
||||||
- [x] Fix Github Actions
|
- [x] Fix Github Actions
|
||||||
- [ ] Better error handling
|
- [ ] Better error handling
|
||||||
- [ ] Fix output image scaling
|
- [x]? Fix output image scaling
|
||||||
- [ ] Use HTML output
|
- [ ] Use HTML output
|
||||||
- [x] Override default equation rendering
|
- [x] Override default equation rendering
|
||||||
- [ ] Custom editor for `.typ` files
|
- [ ] Custom editor for `.typ` files
|
||||||
|
- [ ] Mobile file reading
|
||||||
|
- [ ] Automate package downloading
|
||||||
|
68
compiler.worker.ts
Normal file
68
compiler.worker.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
//@ts-ignore
|
||||||
|
import wasmBin from './pkg/obsidian_typst_bg.wasm'
|
||||||
|
import * as typst from './pkg'
|
||||||
|
|
||||||
|
import { CompileCommand, WorkerRequest } from "types";
|
||||||
|
|
||||||
|
typst.initSync(wasmBin);
|
||||||
|
|
||||||
|
let canUseSharedArrayBuffer = new Boolean(false);
|
||||||
|
|
||||||
|
// let buffer: Int32Array;
|
||||||
|
let decoder = new TextDecoder()
|
||||||
|
|
||||||
|
function requestData(path: string): string {
|
||||||
|
if (!canUseSharedArrayBuffer) {
|
||||||
|
throw "Cannot read files on mobile"
|
||||||
|
}
|
||||||
|
// @ts-expect-error
|
||||||
|
let buffer = new Int32Array(new SharedArrayBuffer(4, { maxByteLength: 1e8 }))
|
||||||
|
buffer[0] = 0;
|
||||||
|
postMessage({ buffer, path })
|
||||||
|
const res = Atomics.wait(buffer, 0, 0);
|
||||||
|
if (buffer[0] == 0) {
|
||||||
|
return decoder.decode(Uint8Array.from(buffer.slice(1)))
|
||||||
|
}
|
||||||
|
|
||||||
|
throw buffer[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
let compiler = new typst.SystemWorld("", requestData)
|
||||||
|
const compileAttempts = 5;
|
||||||
|
|
||||||
|
// function compile(data: CompileCommand) {
|
||||||
|
// for (let i = 1; i <= compileAttempts; i += 1) {
|
||||||
|
// try {
|
||||||
|
// return compiler.compile(data.source, data.path, data.pixel_per_pt, data.fill, data.size, data.display)
|
||||||
|
// } catch (error) {
|
||||||
|
// if (i < compileAttempts && ((error.name == "RuntimeError" && error.message == "unreachable") || (error.name == "Uncaught Error"))) {
|
||||||
|
// console.warn("Typst compiler crashed, attempting to restart: ", i);
|
||||||
|
// console.log(data);
|
||||||
|
|
||||||
|
// compiler.free()
|
||||||
|
// compiler = new typst.SystemWorld("", requestData)
|
||||||
|
// } else {
|
||||||
|
// console.log("name", error.name);
|
||||||
|
// console.log("message", error.message);
|
||||||
|
|
||||||
|
// throw error;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
onmessage = (ev: MessageEvent<CompileCommand | true>) => {
|
||||||
|
if (ev.data == true) {
|
||||||
|
canUseSharedArrayBuffer = ev.data
|
||||||
|
} else if ("source" in ev.data) {
|
||||||
|
const data: CompileCommand = ev.data;
|
||||||
|
postMessage(compiler.compile(data.source, data.path, data.pixel_per_pt, data.fill, data.size, data.display))
|
||||||
|
|
||||||
|
// postMessage(compile(ev.data))
|
||||||
|
} else {
|
||||||
|
throw ev;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("Typst compiler worker loaded!");
|
@ -2,6 +2,8 @@ import esbuild from "esbuild";
|
|||||||
import process from "process";
|
import process from "process";
|
||||||
import builtins from "builtin-modules";
|
import builtins from "builtin-modules";
|
||||||
|
|
||||||
|
import inlineWorkerPlugin from "esbuild-plugin-inline-worker";
|
||||||
|
|
||||||
const banner =
|
const banner =
|
||||||
`/*
|
`/*
|
||||||
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
THIS IS A GENERATED/BUNDLED FILE BY ESBUILD
|
||||||
@ -69,13 +71,16 @@ const context = await esbuild.context({
|
|||||||
treeShaking: true,
|
treeShaking: true,
|
||||||
outfile: "main.js",
|
outfile: "main.js",
|
||||||
plugins: [
|
plugins: [
|
||||||
wasmPlugin
|
inlineWorkerPlugin({ format: "cjs", target: "es2018", plugins: [wasmPlugin] })
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
if (prod) {
|
await context.rebuild();
|
||||||
await context.rebuild();
|
process.exit(0)
|
||||||
process.exit(0);
|
|
||||||
} else {
|
// if (prod) {
|
||||||
await context.watch();
|
// await context.rebuild();
|
||||||
}
|
// process.exit(0);
|
||||||
|
// } else {
|
||||||
|
// await context.watch();
|
||||||
|
// }
|
267
main.ts
267
main.ts
@ -1,11 +1,10 @@
|
|||||||
import { App, HexString, Notice, Plugin, PluginSettingTab, Setting, Workspace, loadMathJax } from 'obsidian';
|
import { App, renderMath, HexString, Platform, Plugin, PluginSettingTab, Setting, loadMathJax, normalizePath } from 'obsidian';
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import typst_wasm_bin from './pkg/obsidian_typst_bg.wasm'
|
import Worker from "./compiler.worker.ts"
|
||||||
import typstInit, * as typst from './pkg/obsidian_typst'
|
|
||||||
import TypstCanvasElement from 'typst-canvas-element';
|
|
||||||
|
|
||||||
// temp.track()
|
import TypstCanvasElement from 'typst-canvas-element';
|
||||||
|
import { WorkerRequest } from 'types.js';
|
||||||
|
|
||||||
interface TypstPluginSettings {
|
interface TypstPluginSettings {
|
||||||
noFill: boolean,
|
noFill: boolean,
|
||||||
@ -27,85 +26,218 @@ const DEFAULT_SETTINGS: TypstPluginSettings = {
|
|||||||
search_system: false,
|
search_system: false,
|
||||||
override_math: false,
|
override_math: false,
|
||||||
preamable: {
|
preamable: {
|
||||||
shared: "#let pxToPt = (p) => if p == auto {p} else {p * DPR * (72/96) * 1pt}\n#set text(fill: white, size: pxToPt(SIZE))",
|
shared: "#set text(fill: white, size: SIZE)\n#set page(width: WIDTH, height: HEIGHT)",
|
||||||
math: "#set page(width: pxToPt(WIDTH), height: pxToPt(HEIGHT), margin: 0pt)\n#set align(horizon)",
|
math: "#set page(margin: 0pt)\n#set align(horizon)",
|
||||||
code: "#set page(width: auto, height: auto, margin: 1em)"
|
code: "#set page(margin: (y: 1em, x: 0pt))"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class TypstPlugin extends Plugin {
|
export default class TypstPlugin extends Plugin {
|
||||||
settings: TypstPluginSettings;
|
settings: TypstPluginSettings;
|
||||||
compiler: typst.SystemWorld;
|
|
||||||
files: Map<string, string>;
|
compilerWorker: Worker;
|
||||||
|
|
||||||
tex2chtml: any;
|
tex2chtml: any;
|
||||||
resizeObserver: ResizeObserver;
|
|
||||||
|
prevCanvasHeight: number = 0;
|
||||||
|
textEncoder: TextEncoder
|
||||||
|
fs: any;
|
||||||
|
|
||||||
async onload() {
|
async onload() {
|
||||||
await typstInit(typst_wasm_bin)
|
this.compilerWorker = new Worker();
|
||||||
|
if (!Platform.isMobileApp) {
|
||||||
|
this.compilerWorker.postMessage(true);
|
||||||
|
this.fs = require("fs")
|
||||||
|
}
|
||||||
|
|
||||||
|
this.textEncoder = new TextEncoder()
|
||||||
await this.loadSettings()
|
await this.loadSettings()
|
||||||
let notice = new Notice("Loading fonts for Typst...");
|
|
||||||
this.compiler = await new typst.SystemWorld("", (path: string) => this.get_file(path), this.settings.search_system);
|
|
||||||
notice.hide();
|
|
||||||
notice = new Notice("Finished loading fonts for Typst", 5000);
|
|
||||||
|
|
||||||
this.registerEvent(
|
TypstCanvasElement.compile = (a, b, c, d, e) => this.processThenCompileTypst(a, b, c, d, e)
|
||||||
this.app.metadataCache.on("resolved", () => this.updateFileCache())
|
if (customElements.get("typst-renderer") == undefined) {
|
||||||
)
|
customElements.define("typst-renderer", TypstCanvasElement, { extends: "canvas" })
|
||||||
|
}
|
||||||
TypstCanvasElement.compile = (a, b, c, d) => this.typstToSizedImage(a, b, c, d)
|
|
||||||
customElements.define("typst-canvas", TypstCanvasElement, { extends: "canvas" })
|
|
||||||
|
|
||||||
await loadMathJax()
|
await loadMathJax()
|
||||||
|
renderMath("", false);
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
this.tex2chtml = MathJax.tex2chtml
|
this.tex2chtml = MathJax.tex2chtml
|
||||||
this.overrideMathJax(this.settings.override_math)
|
this.overrideMathJax(this.settings.override_math)
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: "math-override",
|
id: "toggle-math-override",
|
||||||
name: "Toggle Math Block Override",
|
name: "Toggle math block override",
|
||||||
callback: () => this.overrideMathJax(!this.settings.override_math)
|
callback: () => this.overrideMathJax(!this.settings.override_math)
|
||||||
})
|
})
|
||||||
this.addCommand({
|
|
||||||
id: "update-files",
|
|
||||||
name: "Update Cached .typ Files",
|
|
||||||
callback: () => this.updateFileCache()
|
|
||||||
})
|
|
||||||
|
|
||||||
this.addSettingTab(new TypstSettingTab(this.app, this));
|
this.addSettingTab(new TypstSettingTab(this.app, this));
|
||||||
this.registerMarkdownCodeBlockProcessor("typst", (source, el, ctx) => {
|
this.registerMarkdownCodeBlockProcessor("typst", async (source, el, ctx) => {
|
||||||
el.appendChild(this.typstToCanvas(`${this.settings.preamable.code}\n${source}`, true))
|
el.appendChild(this.createTypstCanvas("/" + ctx.sourcePath, `${this.settings.preamable.code}\n${source}`, true, false))
|
||||||
})
|
})
|
||||||
|
|
||||||
console.log("loaded Typst");
|
|
||||||
|
console.log("loaded Typst Renderer");
|
||||||
}
|
}
|
||||||
|
|
||||||
typstToImage(source: string) {
|
// async loadCompilerWorker() {
|
||||||
return this.compiler.compile(source, this.settings.pixel_per_pt, `${this.settings.fill}${this.settings.noFill ? "00" : "ff"}`)
|
// this.compilerWorker.
|
||||||
|
// }
|
||||||
|
|
||||||
|
async compileToTypst(path: string, source: string, size: number, display: boolean): Promise<ImageData> {
|
||||||
|
return await navigator.locks.request("typst renderer compiler", async (lock) => {
|
||||||
|
this.compilerWorker.postMessage({
|
||||||
|
source,
|
||||||
|
path,
|
||||||
|
pixel_per_pt: this.settings.pixel_per_pt,
|
||||||
|
fill: `${this.settings.fill}${this.settings.noFill ? "00" : "ff"}`,
|
||||||
|
size,
|
||||||
|
display
|
||||||
|
});
|
||||||
|
while (true) {
|
||||||
|
let result: ImageData | WorkerRequest = await new Promise((resolve, reject) => {
|
||||||
|
const listener = (ev: MessageEvent<ImageData>) => {
|
||||||
|
remove();
|
||||||
|
resolve(ev.data);
|
||||||
|
}
|
||||||
|
const errorListener = (error: ErrorEvent) => {
|
||||||
|
remove();
|
||||||
|
reject(error.message)
|
||||||
|
}
|
||||||
|
const remove = () => {
|
||||||
|
this.compilerWorker.removeEventListener("message", listener);
|
||||||
|
this.compilerWorker.removeEventListener("error", errorListener);
|
||||||
|
}
|
||||||
|
this.compilerWorker.addEventListener("message", listener);
|
||||||
|
this.compilerWorker.addEventListener("error", errorListener);
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result instanceof ImageData) {
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
// Cannot reach this point when in mobile app as the worker should
|
||||||
|
// not have a SharedArrayBuffer
|
||||||
|
await this.handleWorkerRequest(result)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
typstToSizedImage(source: string, size: number, display: boolean, fontSize: number) {
|
async handleWorkerRequest({ buffer: wbuffer, path }: WorkerRequest) {
|
||||||
const sizing = `#let (WIDTH, HEIGHT, SIZE, DPR) = (${display ? size : "auto"}, ${!display ? size : "auto"}, ${fontSize}, ${window.devicePixelRatio})`
|
try {
|
||||||
return this.typstToImage(
|
let s = await (
|
||||||
`${sizing}\n${this.settings.preamable.shared}\n${source}`
|
path.startsWith("@")
|
||||||
|
? this.preparePackage(path)
|
||||||
|
: this.getFileString(path)
|
||||||
|
);
|
||||||
|
if (s) {
|
||||||
|
|
||||||
|
|
||||||
|
let buffer = Int32Array.from(this.textEncoder.encode(
|
||||||
|
s
|
||||||
|
));
|
||||||
|
if (wbuffer.byteLength < (buffer.byteLength + 4)) {
|
||||||
|
//@ts-expect-error
|
||||||
|
wbuffer.buffer.grow(buffer.byteLength + 4)
|
||||||
|
}
|
||||||
|
wbuffer.set(buffer, 1)
|
||||||
|
wbuffer[0] = 0
|
||||||
|
} else {
|
||||||
|
wbuffer[0] = -2
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
wbuffer[0] = -1
|
||||||
|
throw error
|
||||||
|
} finally {
|
||||||
|
Atomics.notify(wbuffer, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFileString(path: string): Promise<string> {
|
||||||
|
if (require("path").isAbsolute(path)) {
|
||||||
|
return await this.fs.promises.readFile(path, { encoding: "utf8" })
|
||||||
|
} else {
|
||||||
|
return await this.app.vault.adapter.read(normalizePath(path))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async preparePackage(spec: string): Promise<string | undefined> {
|
||||||
|
spec = spec.slice(1)
|
||||||
|
let subdir = "/typst/packages/" + spec
|
||||||
|
|
||||||
|
let dir = normalizePath(this.getDataDir() + subdir)
|
||||||
|
if (this.fs.existsSync(dir)) {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
dir = normalizePath(this.getCacheDir() + subdir)
|
||||||
|
|
||||||
|
if (this.fs.existsSync(dir)) {
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getDataDir() {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
if ("XDG_DATA_HOME" in process.env) {
|
||||||
|
return process.env["XDG_DATA_HOME"]
|
||||||
|
} else {
|
||||||
|
return process.env["HOME"] + "/.local/share"
|
||||||
|
}
|
||||||
|
} else if (Platform.isWin) {
|
||||||
|
return process.env["APPDATA"]
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return process.env["HOME"] + "/Library/Application Support"
|
||||||
|
}
|
||||||
|
throw "Cannot find data directory on an unknown platform"
|
||||||
|
}
|
||||||
|
|
||||||
|
getCacheDir() {
|
||||||
|
if (Platform.isLinux) {
|
||||||
|
if ("XDG_CACHE_HOME" in process.env) {
|
||||||
|
return process.env["XDG_DATA_HOME"]
|
||||||
|
} else {
|
||||||
|
return process.env["HOME"] + "/.cache"
|
||||||
|
}
|
||||||
|
} else if (Platform.isWin) {
|
||||||
|
return process.env["LOCALAPPDATA"]
|
||||||
|
} else if (Platform.isMacOS) {
|
||||||
|
return process.env["HOME"] + "/Library/Caches"
|
||||||
|
}
|
||||||
|
throw "Cannot find cache directory on an unknown platform"
|
||||||
|
}
|
||||||
|
|
||||||
|
async processThenCompileTypst(path: string, source: string, size: number, display: boolean, fontSize: number) {
|
||||||
|
const dpr = window.devicePixelRatio;
|
||||||
|
const pxToPt = (px: number) => (px * dpr * (72 / 96)).toString() + "pt"
|
||||||
|
const sizing = `#let (WIDTH, HEIGHT, SIZE) = (${display ? pxToPt(size) : "auto"}, ${!display ? pxToPt(size) : "auto"}, ${pxToPt(fontSize)})`
|
||||||
|
return this.compileToTypst(
|
||||||
|
path,
|
||||||
|
`${sizing}\n${this.settings.preamable.shared}\n${source}`,
|
||||||
|
size,
|
||||||
|
display
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
typstToCanvas(source: string, display: boolean) {
|
createTypstCanvas(path: string, source: string, display: boolean, math: boolean) {
|
||||||
let canvas = new TypstCanvasElement();
|
let canvas = new TypstCanvasElement();
|
||||||
canvas.source = source
|
canvas.source = source
|
||||||
|
canvas.path = path
|
||||||
canvas.display = display
|
canvas.display = display
|
||||||
|
canvas.math = math
|
||||||
return canvas
|
return canvas
|
||||||
}
|
}
|
||||||
|
|
||||||
typstMath2Html(source: string, r: { display: boolean }) {
|
createTypstMath(source: string, r: { display: boolean }) {
|
||||||
const display = r.display;
|
const display = r.display;
|
||||||
source = `${this.settings.preamable.math}\n${display ? `$ ${source} $` : `$${source}$`}`
|
source = `${this.settings.preamable.math}\n${display ? `$ ${source} $` : `$${source}$`}`
|
||||||
return this.typstToCanvas(source, display)
|
|
||||||
|
return this.createTypstCanvas("/586f8912-f3a8-4455-8a4a-3729469c2cc1.typ", source, display, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
onunload() {
|
onunload() {
|
||||||
//@ts-expect-error
|
// @ts-expect-error
|
||||||
MathJax.tex2chtml = this.tex2chtml
|
MathJax.tex2chtml = this.tex2chtml
|
||||||
|
this.compilerWorker.terminate()
|
||||||
}
|
}
|
||||||
|
|
||||||
async overrideMathJax(value: boolean) {
|
async overrideMathJax(value: boolean) {
|
||||||
@ -113,7 +245,7 @@ export default class TypstPlugin extends Plugin {
|
|||||||
await this.saveSettings();
|
await this.saveSettings();
|
||||||
if (this.settings.override_math) {
|
if (this.settings.override_math) {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
MathJax.tex2chtml = (e, r) => this.typstMath2Html(e, r)
|
MathJax.tex2chtml = (e, r) => this.createTypstMath(e, r)
|
||||||
} else {
|
} else {
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
MathJax.tex2chtml = this.tex2chtml
|
MathJax.tex2chtml = this.tex2chtml
|
||||||
@ -127,23 +259,6 @@ export default class TypstPlugin extends Plugin {
|
|||||||
async saveSettings() {
|
async saveSettings() {
|
||||||
await this.saveData(this.settings);
|
await this.saveData(this.settings);
|
||||||
}
|
}
|
||||||
|
|
||||||
get_file(path: string) {
|
|
||||||
if (this.files.has(path)) {
|
|
||||||
return this.files.get(path)
|
|
||||||
}
|
|
||||||
console.error(`'${path}' is a folder or does not exist`, this.files.keys());
|
|
||||||
throw `'${path}' is a folder or does not exist`
|
|
||||||
}
|
|
||||||
|
|
||||||
async updateFileCache() {
|
|
||||||
this.files = new Map()
|
|
||||||
for (const file of this.app.vault.getFiles()) {
|
|
||||||
if (file.extension == "typ") {
|
|
||||||
this.files.set(file.path, await this.app.vault.cachedRead(file))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class TypstSettingTab extends PluginSettingTab {
|
class TypstSettingTab extends PluginSettingTab {
|
||||||
@ -159,18 +274,7 @@ class TypstSettingTab extends PluginSettingTab {
|
|||||||
|
|
||||||
containerEl.empty();
|
containerEl.empty();
|
||||||
|
|
||||||
let fill_color = new Setting(containerEl)
|
|
||||||
.setName("Fill Color")
|
|
||||||
.setDisabled(this.plugin.settings.noFill)
|
|
||||||
.addColorPicker((picker) => {
|
|
||||||
picker.setValue(this.plugin.settings.fill)
|
|
||||||
.onChange(
|
|
||||||
async (value) => {
|
|
||||||
this.plugin.settings.fill = value;
|
|
||||||
await this.plugin.saveSettings();
|
|
||||||
}
|
|
||||||
)
|
|
||||||
})
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("No Fill (Transparent)")
|
.setName("No Fill (Transparent)")
|
||||||
.addToggle((toggle) => {
|
.addToggle((toggle) => {
|
||||||
@ -183,6 +287,20 @@ class TypstSettingTab extends PluginSettingTab {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let fill_color = new Setting(containerEl)
|
||||||
|
.setName("Fill Color")
|
||||||
|
.setDisabled(this.plugin.settings.noFill)
|
||||||
|
.addColorPicker((picker) => {
|
||||||
|
picker.setValue(this.plugin.settings.fill)
|
||||||
|
.onChange(
|
||||||
|
async (value) => {
|
||||||
|
this.plugin.settings.fill = value;
|
||||||
|
await this.plugin.saveSettings();
|
||||||
|
}
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Pixel Per Point")
|
.setName("Pixel Per Point")
|
||||||
.addSlider((slider) =>
|
.addSlider((slider) =>
|
||||||
@ -193,7 +311,10 @@ class TypstSettingTab extends PluginSettingTab {
|
|||||||
this.plugin.settings.pixel_per_pt = value;
|
this.plugin.settings.pixel_per_pt = value;
|
||||||
await this.plugin.saveSettings();
|
await this.plugin.saveSettings();
|
||||||
}
|
}
|
||||||
))
|
)
|
||||||
|
.setDynamicTooltip()
|
||||||
|
)
|
||||||
|
|
||||||
new Setting(containerEl)
|
new Setting(containerEl)
|
||||||
.setName("Search System Fonts")
|
.setName("Search System Fonts")
|
||||||
.setDesc(`Whether the plugin should search for system fonts.
|
.setDesc(`Whether the plugin should search for system fonts.
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"id": "typst",
|
"id": "typst",
|
||||||
"name": "Typst Renderer",
|
"name": "Typst Renderer",
|
||||||
"version": "0.4.2",
|
"version": "0.5.0",
|
||||||
"minAppVersion": "1.0.0",
|
"minAppVersion": "1.0.0",
|
||||||
"description": "Renders `typst` code blocks and math blocks to images with Typst.",
|
"description": "Renders `typst` code blocks and math blocks to images with Typst.",
|
||||||
"author": "fenjalien",
|
"author": "fenjalien",
|
||||||
|
741
package-lock.json
generated
741
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
29
package.json
29
package.json
@ -1,11 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "obsidian-typst-plugin",
|
"name": "obsidian-typst-plugin",
|
||||||
"version": "0.4.2",
|
"version": "0.5.0",
|
||||||
"description": "Renders `typst` code blocks to images with Typst.",
|
"description": "Renders `typst` code blocks to images with Typst.",
|
||||||
"main": "main.js",
|
"main": "main.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"wasm": "wasm-pack build --target web",
|
"wasm": "wasm-pack build --target web",
|
||||||
"dev": "wasm-pack build --target web --dev && node esbuild.config.mjs",
|
"dev": "node esbuild.config.mjs",
|
||||||
|
"wasm-dev": "wasm-pack build --target web --dev && node esbuild.config.mjs",
|
||||||
"wasm-build": "wasm-pack build --target web && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"wasm-build": "wasm-pack build --target web && tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
"build": "tsc -noEmit -skipLibCheck && node esbuild.config.mjs production",
|
||||||
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
"version": "node version-bump.mjs && git add manifest.json versions.json"
|
||||||
@ -14,23 +15,17 @@
|
|||||||
"author": "fenjalien",
|
"author": "fenjalien",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^16.11.6",
|
"@types/node": "^20",
|
||||||
"@types/temp": "^0.9.1",
|
"@typescript-eslint/eslint-plugin": "^5",
|
||||||
"@typescript-eslint/eslint-plugin": "5.29.0",
|
"@typescript-eslint/parser": "^5",
|
||||||
"@typescript-eslint/parser": "5.29.0",
|
"builtin-modules": "^3",
|
||||||
"builtin-modules": "3.3.0",
|
"esbuild": "^0.18",
|
||||||
"esbuild": "0.17.3",
|
"esbuild-plugin-inline-worker": "^0.1.1",
|
||||||
"esbuild-plugin-wasm": "^1.0.0",
|
"typescript": "^5.1"
|
||||||
"obsidian": "latest",
|
|
||||||
"tslib": "2.4.0",
|
|
||||||
"typescript": "4.7.4"
|
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@codemirror/language": "^6.6.0",
|
"obsidian": "latest",
|
||||||
"@codemirror/state": "^6.2.0",
|
|
||||||
"@codemirror/view": "^6.10.0",
|
|
||||||
"@lezer/common": "^1.0.2",
|
|
||||||
"obsidian-typst": "file:pkg",
|
"obsidian-typst": "file:pkg",
|
||||||
"temp": "^0.9.4"
|
"tslib": "^2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
49
src/fonts.rs
Normal file
49
src/fonts.rs
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
use typst::{
|
||||||
|
font::{Font, FontBook},
|
||||||
|
util::Bytes,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Searches for fonts.
|
||||||
|
pub struct FontSearcher {
|
||||||
|
/// Metadata about all discovered fonts.
|
||||||
|
pub book: FontBook,
|
||||||
|
/// Slots that the fonts are loaded into.
|
||||||
|
pub fonts: Vec<Font>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FontSearcher {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
book: FontBook::new(),
|
||||||
|
fonts: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_embedded(&mut self) {
|
||||||
|
let mut process = |bytes: &'static [u8]| {
|
||||||
|
let buffer = Bytes::from_static(bytes);
|
||||||
|
for font in Font::iter(buffer) {
|
||||||
|
self.book.push(font.info().clone());
|
||||||
|
self.fonts.push(font);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
macro_rules! add {
|
||||||
|
($filename:literal) => {
|
||||||
|
process(include_bytes!(concat!("../assets/fonts/", $filename)));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embed default fonts.
|
||||||
|
add!("LinLibertine_R.ttf");
|
||||||
|
add!("LinLibertine_RB.ttf");
|
||||||
|
add!("LinLibertine_RBI.ttf");
|
||||||
|
add!("LinLibertine_RI.ttf");
|
||||||
|
add!("NewCMMath-Book.otf");
|
||||||
|
add!("NewCMMath-Regular.otf");
|
||||||
|
add!("DejaVuSansMono.ttf");
|
||||||
|
add!("DejaVuSansMono-Bold.ttf");
|
||||||
|
add!("DejaVuSansMono-Oblique.ttf");
|
||||||
|
add!("DejaVuSansMono-BoldOblique.ttf");
|
||||||
|
}
|
||||||
|
}
|
434
src/lib.rs
434
src/lib.rs
@ -1,299 +1,279 @@
|
|||||||
use comemo::Prehashed;
|
use comemo::Prehashed;
|
||||||
use elsa::FrozenVec;
|
use fast_image_resize as fr;
|
||||||
use once_cell::unsync::OnceCell;
|
|
||||||
use siphasher::sip128::{Hasher128, SipHasher};
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::{RefCell, RefMut},
|
cell::{OnceCell, RefCell, RefMut},
|
||||||
collections::HashMap,
|
collections::HashMap,
|
||||||
hash::Hash,
|
num::NonZeroU32,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
str::FromStr,
|
str::FromStr,
|
||||||
};
|
};
|
||||||
use typst::{
|
use typst::{
|
||||||
diag::{FileError, FileResult},
|
diag::{EcoString, FileError, FileResult, PackageError, PackageResult},
|
||||||
eval::Library,
|
eval::{Datetime, Library},
|
||||||
font::{Font, FontBook, FontInfo},
|
file::{FileId, PackageSpec},
|
||||||
|
font::{Font, FontBook},
|
||||||
geom::{Color, RgbaColor},
|
geom::{Color, RgbaColor},
|
||||||
syntax::{Source, SourceId},
|
syntax::Source,
|
||||||
util::{Buffer, PathExt},
|
util::{Bytes, PathExt},
|
||||||
World,
|
World,
|
||||||
};
|
};
|
||||||
use wasm_bindgen::{prelude::*, Clamped};
|
use wasm_bindgen::{prelude::*, Clamped};
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use web_sys::ImageData;
|
||||||
use web_sys::{console, Blob, FontData, ImageData};
|
|
||||||
|
|
||||||
#[wasm_bindgen]
|
mod fonts;
|
||||||
extern "C" {
|
mod paths;
|
||||||
fn alert(s: &str);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[wasm_bindgen(module = "fs")]
|
use crate::fonts::FontSearcher;
|
||||||
extern "C" {
|
use crate::paths::{PathHash, PathSlot};
|
||||||
#[wasm_bindgen(catch)]
|
|
||||||
fn readFileSync(path: &str) -> Result<JsValue, JsValue>;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A world that provides access to the operating system.
|
/// A world that provides access to the operating system.
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct SystemWorld {
|
pub struct SystemWorld {
|
||||||
|
/// The root relative to which absolute paths are resolved.
|
||||||
root: PathBuf,
|
root: PathBuf,
|
||||||
|
/// The input source.
|
||||||
|
main: FileId,
|
||||||
|
/// Typst's standard library.
|
||||||
library: Prehashed<Library>,
|
library: Prehashed<Library>,
|
||||||
|
/// Metadata about discovered fonts.
|
||||||
book: Prehashed<FontBook>,
|
book: Prehashed<FontBook>,
|
||||||
fonts: Vec<FontSlot>,
|
/// Storage of fonts
|
||||||
hashes: RefCell<HashMap<PathBuf, PathHash>>,
|
fonts: Vec<Font>,
|
||||||
|
/// Maps package-path combinations to canonical hashes. All package-path
|
||||||
|
/// combinations that point to thes same file are mapped to the same hash. To
|
||||||
|
/// be used in conjunction with `paths`.
|
||||||
|
hashes: RefCell<HashMap<FileId, FileResult<PathHash>>>,
|
||||||
|
/// Maps canonical path hashes to source files and buffers.
|
||||||
paths: RefCell<HashMap<PathHash, PathSlot>>,
|
paths: RefCell<HashMap<PathHash, PathSlot>>,
|
||||||
sources: FrozenVec<Box<Source>>,
|
/// The current date if requested. This is stored here to ensure it is
|
||||||
main: SourceId,
|
/// always the same within one compilation. Reset between compilations.
|
||||||
js_read_file: js_sys::Function,
|
today: OnceCell<Option<Datetime>>,
|
||||||
|
|
||||||
|
packages: RefCell<HashMap<PackageSpec, PackageResult<PathBuf>>>,
|
||||||
|
|
||||||
|
resizer: fr::Resizer,
|
||||||
|
|
||||||
|
js_request_data: js_sys::Function,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
impl SystemWorld {
|
impl SystemWorld {
|
||||||
#[wasm_bindgen(constructor)]
|
#[wasm_bindgen(constructor)]
|
||||||
pub async fn new(
|
pub fn new(root: String, js_read_file: &js_sys::Function) -> SystemWorld {
|
||||||
root: String,
|
console_error_panic_hook::set_once();
|
||||||
js_read_file: &js_sys::Function,
|
|
||||||
search_system: bool,
|
|
||||||
) -> Result<SystemWorld, JsValue> {
|
|
||||||
let mut searcher = FontSearcher::new();
|
let mut searcher = FontSearcher::new();
|
||||||
if search_system {
|
searcher.add_embedded();
|
||||||
searcher.search_system().await?;
|
|
||||||
} else {
|
|
||||||
searcher.add_embedded();
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(Self {
|
Self {
|
||||||
root: PathBuf::from(root),
|
root: PathBuf::from(root),
|
||||||
|
main: FileId::detached(),
|
||||||
library: Prehashed::new(typst_library::build()),
|
library: Prehashed::new(typst_library::build()),
|
||||||
book: Prehashed::new(searcher.book),
|
book: Prehashed::new(searcher.book),
|
||||||
fonts: searcher.fonts,
|
fonts: searcher.fonts,
|
||||||
hashes: RefCell::default(),
|
hashes: RefCell::default(),
|
||||||
paths: RefCell::default(),
|
paths: RefCell::default(),
|
||||||
sources: FrozenVec::new(),
|
today: OnceCell::new(),
|
||||||
main: SourceId::detached(),
|
packages: RefCell::default(),
|
||||||
js_read_file: js_read_file.clone(),
|
resizer: fr::Resizer::default(),
|
||||||
})
|
js_request_data: js_read_file.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reset(&mut self) {
|
||||||
|
self.hashes.borrow_mut().clear();
|
||||||
|
self.paths.borrow_mut().clear();
|
||||||
|
self.today.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn compile(
|
pub fn compile(
|
||||||
&mut self,
|
&mut self,
|
||||||
source: String,
|
text: String,
|
||||||
|
path: String,
|
||||||
pixel_per_pt: f32,
|
pixel_per_pt: f32,
|
||||||
fill: String,
|
fill: String,
|
||||||
|
size: u32,
|
||||||
|
display: bool,
|
||||||
) -> Result<ImageData, JsValue> {
|
) -> Result<ImageData, JsValue> {
|
||||||
self.sources.as_mut().clear();
|
self.reset();
|
||||||
self.hashes.borrow_mut().clear();
|
|
||||||
self.paths.borrow_mut().clear();
|
// Insert the main path slot
|
||||||
|
let system_path = PathBuf::from(path);
|
||||||
|
let hash = PathHash::new(&text);
|
||||||
|
self.main = FileId::new(None, &system_path);
|
||||||
|
self.hashes.borrow_mut().insert(self.main, Ok(hash));
|
||||||
|
self.paths.borrow_mut().insert(
|
||||||
|
hash,
|
||||||
|
PathSlot {
|
||||||
|
id: self.main,
|
||||||
|
system_path,
|
||||||
|
buffer: OnceCell::new(),
|
||||||
|
source: Ok(Source::new(self.main, text)),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
self.main = self.insert("<user input>".as_ref(), source);
|
|
||||||
match typst::compile(self) {
|
match typst::compile(self) {
|
||||||
Ok(document) => {
|
Ok(document) => {
|
||||||
let render = typst::export::render(
|
let mut pixmap = typst::export::render(
|
||||||
&document.pages[0],
|
&document.pages[0],
|
||||||
pixel_per_pt,
|
pixel_per_pt,
|
||||||
Color::Rgba(RgbaColor::from_str(&fill)?),
|
Color::Rgba(RgbaColor::from_str(&fill)?),
|
||||||
);
|
);
|
||||||
Ok(ImageData::new_with_u8_clamped_array_and_sh(
|
|
||||||
Clamped(render.data()),
|
let width = pixmap.width();
|
||||||
render.width(),
|
let height = pixmap.height();
|
||||||
render.height(),
|
// Create src image
|
||||||
)?)
|
let mut src_image = fr::Image::from_slice_u8(
|
||||||
|
NonZeroU32::new(width).unwrap(),
|
||||||
|
NonZeroU32::new(height).unwrap(),
|
||||||
|
pixmap.data_mut(),
|
||||||
|
fr::PixelType::U8x4,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
// Multiple RGB channels of source image by alpha channel
|
||||||
|
let alpha_mul_div = fr::MulDiv::default();
|
||||||
|
alpha_mul_div
|
||||||
|
.multiply_alpha_inplace(&mut src_image.view_mut())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let dst_width = NonZeroU32::new(if display {
|
||||||
|
size
|
||||||
|
} else {
|
||||||
|
((size as f32 / height as f32) * width as f32) as u32
|
||||||
|
})
|
||||||
|
.unwrap_or(NonZeroU32::MIN);
|
||||||
|
let dst_height = NonZeroU32::new(if display {
|
||||||
|
((size as f32 / width as f32) * height as f32) as u32
|
||||||
|
} else {
|
||||||
|
size
|
||||||
|
})
|
||||||
|
.unwrap_or(NonZeroU32::MIN);
|
||||||
|
|
||||||
|
// Create container for data of destination image
|
||||||
|
let mut dst_image = fr::Image::new(dst_width, dst_height, src_image.pixel_type());
|
||||||
|
// Get mutable view of destination image data
|
||||||
|
let mut dst_view = dst_image.view_mut();
|
||||||
|
|
||||||
|
// Resize source image into buffer of destination image
|
||||||
|
self.resizer
|
||||||
|
.resize(&src_image.view(), &mut dst_view)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
alpha_mul_div.divide_alpha_inplace(&mut dst_view).unwrap();
|
||||||
|
|
||||||
|
ImageData::new_with_u8_clamped_array_and_sh(
|
||||||
|
Clamped(dst_image.buffer()),
|
||||||
|
dst_width.get(),
|
||||||
|
dst_height.get(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Err(errors) => Err(format!("{:?}", *errors).into()),
|
Err(errors) => Err(format!(
|
||||||
|
"{:?}",
|
||||||
|
errors
|
||||||
|
.into_iter()
|
||||||
|
.map(|e| e.message)
|
||||||
|
.collect::<Vec<EcoString>>()
|
||||||
|
)
|
||||||
|
.into()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl World for SystemWorld {
|
impl World for SystemWorld {
|
||||||
fn root(&self) -> &Path {
|
|
||||||
&self.root
|
|
||||||
}
|
|
||||||
|
|
||||||
fn library(&self) -> &Prehashed<Library> {
|
fn library(&self) -> &Prehashed<Library> {
|
||||||
&self.library
|
&self.library
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main(&self) -> &Source {
|
|
||||||
self.source(self.main)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve(&self, path: &Path) -> FileResult<SourceId> {
|
|
||||||
let path = self.root.join(path);
|
|
||||||
let path = path.as_path();
|
|
||||||
self.slot(path)?
|
|
||||||
.source
|
|
||||||
.get_or_init(|| {
|
|
||||||
let buf = self.read_file(path)?;
|
|
||||||
let text = String::from_utf8(buf)?;
|
|
||||||
Ok(self.insert(path, text))
|
|
||||||
})
|
|
||||||
.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn source(&self, id: SourceId) -> &Source {
|
|
||||||
&self.sources[id.into_u16() as usize]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn book(&self) -> &Prehashed<FontBook> {
|
fn book(&self) -> &Prehashed<FontBook> {
|
||||||
&self.book
|
&self.book
|
||||||
}
|
}
|
||||||
|
|
||||||
fn font(&self, id: usize) -> Option<Font> {
|
fn main(&self) -> Source {
|
||||||
let slot = &self.fonts[id];
|
self.source(self.main).unwrap()
|
||||||
slot.font
|
|
||||||
.get_or_init(|| Font::new(slot.buffer.clone(), slot.index))
|
|
||||||
.clone()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file(&self, path: &Path) -> FileResult<Buffer> {
|
fn source(&self, id: FileId) -> FileResult<Source> {
|
||||||
let path = self.root.join(path);
|
self.slot(id)?.source()
|
||||||
let path = path.as_path();
|
}
|
||||||
self.slot(path)?
|
|
||||||
.buffer
|
fn file(&self, id: FileId) -> FileResult<Bytes> {
|
||||||
.get_or_init(|| self.read_file(path).map(Buffer::from))
|
self.slot(id)?.file()
|
||||||
.clone()
|
}
|
||||||
|
|
||||||
|
fn font(&self, index: usize) -> Option<Font> {
|
||||||
|
Some(self.fonts[index].clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn today(&self, _: Option<i64>) -> Option<Datetime> {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SystemWorld {
|
impl SystemWorld {
|
||||||
fn slot(&self, path: &Path) -> FileResult<RefMut<PathSlot>> {
|
fn read_file(&self, path: &Path) -> FileResult<String> {
|
||||||
let mut hashes = self.hashes.borrow_mut();
|
let f = |_e: JsValue| FileError::Other;
|
||||||
let hash = match hashes.get(path).cloned() {
|
Ok(self
|
||||||
Some(hash) => hash,
|
.js_request_data
|
||||||
None => {
|
.call1(&JsValue::NULL, &path.to_str().unwrap().into())
|
||||||
let hash = PathHash::new(Buffer::from(self.read_file(path)?));
|
.map_err(f)?
|
||||||
if let Ok(canon) = path.canonicalize() {
|
.as_string()
|
||||||
hashes.insert(canon.normalize(), hash);
|
.unwrap())
|
||||||
}
|
}
|
||||||
hashes.insert(path.into(), hash);
|
|
||||||
hash
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(std::cell::RefMut::map(self.paths.borrow_mut(), |paths| {
|
fn prepare_package(&self, spec: &PackageSpec) -> PackageResult<PathBuf> {
|
||||||
paths.entry(hash).or_default()
|
let f = |e: JsValue| {
|
||||||
|
if let Some(num) = e.as_f64() {
|
||||||
|
if num == -2.0 {
|
||||||
|
return PackageError::NotFound(spec.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
PackageError::Other
|
||||||
|
};
|
||||||
|
self.packages
|
||||||
|
.borrow_mut()
|
||||||
|
.entry(spec.clone())
|
||||||
|
.or_insert_with(|| {
|
||||||
|
Ok(self
|
||||||
|
.js_request_data
|
||||||
|
.call1(
|
||||||
|
&JsValue::NULL,
|
||||||
|
&format!("@{}/{}-{}", spec.namespace, spec.name, spec.version).into(),
|
||||||
|
)
|
||||||
|
.map_err(f)?
|
||||||
|
.as_string()
|
||||||
|
.unwrap()
|
||||||
|
.into())
|
||||||
|
})
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn slot(&self, id: FileId) -> FileResult<RefMut<PathSlot>> {
|
||||||
|
let mut system_path = PathBuf::new();
|
||||||
|
let mut text = String::new();
|
||||||
|
let hash = self
|
||||||
|
.hashes
|
||||||
|
.borrow_mut()
|
||||||
|
.entry(id)
|
||||||
|
.or_insert_with(|| {
|
||||||
|
let root = match id.package() {
|
||||||
|
Some(spec) => self.prepare_package(spec)?,
|
||||||
|
None => self.root.clone(),
|
||||||
|
};
|
||||||
|
|
||||||
|
system_path = root.join_rooted(id.path()).ok_or(FileError::AccessDenied)?;
|
||||||
|
text = self.read_file(&system_path)?;
|
||||||
|
|
||||||
|
Ok(PathHash::new(&text))
|
||||||
|
})
|
||||||
|
.clone()?;
|
||||||
|
|
||||||
|
Ok(RefMut::map(self.paths.borrow_mut(), |paths| {
|
||||||
|
paths.entry(hash).or_insert_with(|| PathSlot {
|
||||||
|
id,
|
||||||
|
source: Ok(Source::new(id, text)),
|
||||||
|
buffer: OnceCell::new(),
|
||||||
|
system_path,
|
||||||
|
})
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&self, path: &Path, text: String) -> SourceId {
|
|
||||||
let id = SourceId::from_u16(self.sources.len() as u16);
|
|
||||||
let source = Source::new(id, path, text);
|
|
||||||
self.sources.push(Box::new(source));
|
|
||||||
id
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_file(&self, path: &Path) -> FileResult<Vec<u8>> {
|
|
||||||
let f1 = |e: JsValue| {
|
|
||||||
console::error_1(&e);
|
|
||||||
FileError::Other
|
|
||||||
};
|
|
||||||
Ok(self
|
|
||||||
.js_read_file
|
|
||||||
.call1(&JsValue::NULL, &path.to_str().unwrap().into())
|
|
||||||
.map_err(f1)?
|
|
||||||
.as_string()
|
|
||||||
.unwrap()
|
|
||||||
.into_bytes())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds details about the location of a font and lazily the font itself.
|
|
||||||
struct FontSlot {
|
|
||||||
buffer: Buffer,
|
|
||||||
index: u32,
|
|
||||||
font: OnceCell<Option<Font>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A hash that is the same for all paths pointing to the same entity.
|
|
||||||
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
|
||||||
struct PathHash(u128);
|
|
||||||
|
|
||||||
impl PathHash {
|
|
||||||
fn new(handle: Buffer) -> Self {
|
|
||||||
// let handle = Buffer::from(read(path)?);
|
|
||||||
let mut state = SipHasher::new();
|
|
||||||
handle.hash(&mut state);
|
|
||||||
Self(state.finish128().as_u128())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Holds canonical data for all paths pointing to the same entity.
|
|
||||||
#[derive(Default)]
|
|
||||||
struct PathSlot {
|
|
||||||
source: OnceCell<FileResult<SourceId>>,
|
|
||||||
buffer: OnceCell<FileResult<Buffer>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct FontSearcher {
|
|
||||||
book: FontBook,
|
|
||||||
fonts: Vec<FontSlot>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FontSearcher {
|
|
||||||
fn new() -> Self {
|
|
||||||
Self {
|
|
||||||
book: FontBook::new(),
|
|
||||||
fonts: vec![],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_embedded(&mut self) {
|
|
||||||
let mut add = |bytes: &'static [u8]| {
|
|
||||||
let buffer = Buffer::from_static(bytes);
|
|
||||||
for (i, font) in Font::iter(buffer.clone()).enumerate() {
|
|
||||||
self.book.push(font.info().clone());
|
|
||||||
self.fonts.push(FontSlot {
|
|
||||||
buffer: buffer.clone(),
|
|
||||||
index: i as u32,
|
|
||||||
font: OnceCell::from(Some(font)),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Embed default fonts.
|
|
||||||
add(include_bytes!("../assets/fonts/LinLibertine_R.ttf"));
|
|
||||||
add(include_bytes!("../assets/fonts/LinLibertine_RB.ttf"));
|
|
||||||
add(include_bytes!("../assets/fonts/LinLibertine_RBI.ttf"));
|
|
||||||
add(include_bytes!("../assets/fonts/LinLibertine_RI.ttf"));
|
|
||||||
add(include_bytes!("../assets/fonts/NewCMMath-Book.otf"));
|
|
||||||
add(include_bytes!("../assets/fonts/NewCMMath-Regular.otf"));
|
|
||||||
add(include_bytes!("../assets/fonts/DejaVuSansMono.ttf"));
|
|
||||||
add(include_bytes!("../assets/fonts/DejaVuSansMono-Bold.ttf"));
|
|
||||||
add(include_bytes!("../assets/fonts/DejaVuSansMono-Oblique.ttf"));
|
|
||||||
add(include_bytes!(
|
|
||||||
"../assets/fonts/DejaVuSansMono-BoldOblique.ttf"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn search_system(&mut self) -> Result<(), JsValue> {
|
|
||||||
if let Some(window) = web_sys::window() {
|
|
||||||
for fontdata in JsFuture::from(window.query_local_fonts()?)
|
|
||||||
.await?
|
|
||||||
.dyn_into::<js_sys::Array>()?
|
|
||||||
.to_vec()
|
|
||||||
{
|
|
||||||
let buffer = Buffer::from(
|
|
||||||
js_sys::Uint8Array::new(
|
|
||||||
&JsFuture::from(
|
|
||||||
JsFuture::from(fontdata.dyn_into::<FontData>()?.blob())
|
|
||||||
.await?
|
|
||||||
.dyn_into::<Blob>()?
|
|
||||||
.array_buffer(),
|
|
||||||
)
|
|
||||||
.await?,
|
|
||||||
)
|
|
||||||
.to_vec(),
|
|
||||||
);
|
|
||||||
for (i, info) in FontInfo::iter(buffer.as_slice()).enumerate() {
|
|
||||||
self.book.push(info);
|
|
||||||
self.fonts.push(FontSlot {
|
|
||||||
buffer: buffer.clone(),
|
|
||||||
index: i as u32,
|
|
||||||
font: OnceCell::new(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
56
src/paths.rs
Normal file
56
src/paths.rs
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
use std::{cell::OnceCell, hash::Hash, path::PathBuf};
|
||||||
|
|
||||||
|
use siphasher::sip128::{Hasher128, SipHasher13};
|
||||||
|
use typst::{diag::FileResult, file::FileId, syntax::Source, util::Bytes};
|
||||||
|
|
||||||
|
/// Holds canonical data for all pahts pointing to the same entity.
|
||||||
|
///
|
||||||
|
/// Both fields can be populated if the file is both imported and read().
|
||||||
|
pub struct PathSlot {
|
||||||
|
/// The slot's canonical file id.
|
||||||
|
pub id: FileId,
|
||||||
|
/// The slot's path on the system.
|
||||||
|
pub system_path: PathBuf,
|
||||||
|
/// The loaded buffer for a path hash.
|
||||||
|
pub buffer: OnceCell<FileResult<Bytes>>,
|
||||||
|
/// The lazily loaded source file for a path hash.
|
||||||
|
pub source: FileResult<Source>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PathSlot {
|
||||||
|
pub fn source(&self) -> FileResult<Source> {
|
||||||
|
self.source.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn file(&self) -> FileResult<Bytes> {
|
||||||
|
self.buffer
|
||||||
|
.get_or_init(|| Ok(Bytes::from(self.source()?.text().as_bytes())))
|
||||||
|
.clone()
|
||||||
|
}
|
||||||
|
// pub fn source(&self) -> FileResult<Source> {
|
||||||
|
// self.source
|
||||||
|
// .get_or_init(|| {
|
||||||
|
// Ok(Source::new(
|
||||||
|
// self.id,
|
||||||
|
// String::from_utf8(self.buffer.clone()?.to_vec())?,
|
||||||
|
// ))
|
||||||
|
// })
|
||||||
|
// .clone()
|
||||||
|
// }
|
||||||
|
|
||||||
|
// pub fn file(&self) -> FileResult<Bytes> {
|
||||||
|
// self.buffer.clone()
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A hash that is the same for all paths pointing to the same entity.
|
||||||
|
#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash)]
|
||||||
|
pub struct PathHash(u128);
|
||||||
|
|
||||||
|
impl PathHash {
|
||||||
|
pub fn new(source: &str) -> Self {
|
||||||
|
let mut state = SipHasher13::new();
|
||||||
|
source.hash(&mut state);
|
||||||
|
Self(state.finish128().as_u128())
|
||||||
|
}
|
||||||
|
}
|
16
styles.css
16
styles.css
@ -1 +1,15 @@
|
|||||||
/* styles */
|
/* styles */
|
||||||
|
textarea {
|
||||||
|
width: 100%;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.setting-item:has(textarea) {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5em;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-disabled {
|
||||||
|
display: none;
|
||||||
|
}
|
@ -10,7 +10,7 @@
|
|||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"strictNullChecks": true,
|
"strictNullChecks": true,
|
||||||
"lib": [
|
"lib": [
|
||||||
"DOM",
|
"DOM",
|
||||||
"ES5",
|
"ES5",
|
||||||
@ -21,4 +21,4 @@
|
|||||||
"include": [
|
"include": [
|
||||||
"**/*.ts"
|
"**/*.ts"
|
||||||
]
|
]
|
||||||
}
|
}
|
13
types.d.ts
vendored
Normal file
13
types.d.ts
vendored
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
export interface CompileCommand {
|
||||||
|
source: string;
|
||||||
|
path: string;
|
||||||
|
pixel_per_pt: number;
|
||||||
|
fill: string;
|
||||||
|
size: number;
|
||||||
|
display: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface WorkerRequest {
|
||||||
|
buffer: Int32Array,
|
||||||
|
path: string
|
||||||
|
}
|
@ -1,17 +1,31 @@
|
|||||||
export default class TypstCanvasElement extends HTMLCanvasElement {
|
export default class TypstCanvasElement extends HTMLCanvasElement {
|
||||||
static compile: (source: string, size: number, display: boolean, fontSize: number) => ImageData;
|
static compile: (path: string, source: string, size: number, display: boolean, fontSize: number) => Promise<ImageData>;
|
||||||
|
static nextId = 0;
|
||||||
|
static prevHeight = 0;
|
||||||
|
|
||||||
|
id: string
|
||||||
|
abortController: AbortController
|
||||||
source: string
|
source: string
|
||||||
|
path: string
|
||||||
display: boolean
|
display: boolean
|
||||||
resizeObserver: ResizeObserver
|
resizeObserver: ResizeObserver
|
||||||
size: number
|
size: number
|
||||||
|
math: boolean
|
||||||
|
|
||||||
connectedCallback() {
|
async connectedCallback() {
|
||||||
if (!this.isConnected) {
|
if (!this.isConnected) {
|
||||||
console.log("called before connection");
|
console.warn("Typst Renderer: Canvas element has been called before connection");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.draw()
|
|
||||||
|
// if (this.display && this.math) {
|
||||||
|
this.height = TypstCanvasElement.prevHeight;
|
||||||
|
// }
|
||||||
|
|
||||||
|
this.id = "TypstCanvasElement-" + TypstCanvasElement.nextId.toString()
|
||||||
|
TypstCanvasElement.nextId += 1
|
||||||
|
this.abortController = new AbortController()
|
||||||
|
|
||||||
if (this.display) {
|
if (this.display) {
|
||||||
this.resizeObserver = new ResizeObserver((entries) => {
|
this.resizeObserver = new ResizeObserver((entries) => {
|
||||||
if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) {
|
if (entries[0]?.contentBoxSize[0].inlineSize !== this.size) {
|
||||||
@ -20,43 +34,58 @@ export default class TypstCanvasElement extends HTMLCanvasElement {
|
|||||||
})
|
})
|
||||||
this.resizeObserver.observe(this.parentElement!.parentElement!)
|
this.resizeObserver.observe(this.parentElement!.parentElement!)
|
||||||
}
|
}
|
||||||
|
await this.draw()
|
||||||
}
|
}
|
||||||
|
|
||||||
disconnectedCallback() {
|
disconnectedCallback() {
|
||||||
if (this.display) {
|
TypstCanvasElement.prevHeight = this.height
|
||||||
|
if (this.display && this.resizeObserver != undefined) {
|
||||||
this.resizeObserver.disconnect()
|
this.resizeObserver.disconnect()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
draw() {
|
async draw() {
|
||||||
|
this.abortController.abort()
|
||||||
let fontSize = parseFloat(this.getCssPropertyValue("--font-text-size"))
|
this.abortController = new AbortController()
|
||||||
this.size = this.display ? this.parentElement!.parentElement!.innerWidth : parseFloat(this.getCssPropertyValue("--line-height-normal")) * fontSize
|
|
||||||
// console.log(size, this.parentElement);
|
|
||||||
|
|
||||||
|
|
||||||
if (this.display) {
|
|
||||||
this.style.width = "100%"
|
|
||||||
} else {
|
|
||||||
this.style.verticalAlign = "bottom"
|
|
||||||
this.style.height = `${this.size}px`
|
|
||||||
}
|
|
||||||
|
|
||||||
let image: ImageData;
|
|
||||||
let ctx = this.getContext("2d")!;
|
|
||||||
try {
|
try {
|
||||||
image = TypstCanvasElement.compile(this.source, this.size, this.display, fontSize)
|
await navigator.locks.request(this.id, { signal: this.abortController.signal }, async () => {
|
||||||
|
let fontSize = parseFloat(this.getCssPropertyValue("--font-text-size"))
|
||||||
|
this.size = this.display ? this.parentElement!.parentElement!.innerWidth : parseFloat(this.getCssPropertyValue("--line-height-normal")) * fontSize
|
||||||
|
|
||||||
|
// resizeObserver can trigger before the element gets disconnected which can cause the size to be 0
|
||||||
|
// which causes a NaN. size can also sometimes be -ve so wait for resize to draw it again
|
||||||
|
if (this.size <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let image: ImageData;
|
||||||
|
let ctx = this.getContext("2d")!;
|
||||||
|
try {
|
||||||
|
image =
|
||||||
|
await TypstCanvasElement.compile(this.path, this.source, this.size, this.display, fontSize)
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
this.outerText = error
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.display) {
|
||||||
|
this.style.width = "100%"
|
||||||
|
this.style.height = ""
|
||||||
|
} else {
|
||||||
|
this.style.verticalAlign = "bottom"
|
||||||
|
this.style.height = `${this.size}px`
|
||||||
|
}
|
||||||
|
this.width = image.width
|
||||||
|
this.height = image.height
|
||||||
|
|
||||||
|
ctx.imageSmoothingEnabled = true
|
||||||
|
ctx.imageSmoothingQuality = "high"
|
||||||
|
ctx.putImageData(image, 0, 0);
|
||||||
|
})
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
|
||||||
this.outerText = error
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
this.width = image.width
|
|
||||||
this.height = image.height
|
|
||||||
|
|
||||||
ctx.imageSmoothingEnabled = true
|
|
||||||
ctx.imageSmoothingQuality = "high"
|
|
||||||
ctx.putImageData(image, 0, 0);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"0.5.0": "1.0.0",
|
||||||
"0.4.2": "1.0.0",
|
"0.4.2": "1.0.0",
|
||||||
"0.4.1": "1.0.0",
|
"0.4.1": "1.0.0",
|
||||||
"0.4.0": "1.0.0",
|
"0.4.0": "1.0.0",
|
||||||
|
Reference in New Issue
Block a user