WIP: Added canvas viewer for PDF as well

This commit is contained in:
abawi
2025-12-12 14:14:56 +01:00
parent f5c044956f
commit 46989ff0bb
21 changed files with 181 additions and 58 deletions

View File

@@ -1,6 +1,8 @@
// extras/renderers/canvas/CanvasRenderer.tsx
import { t } from '@/i18n';
import type React from 'react';
import { useCallback, useEffect, useRef, useState } from 'react';
import * as pdfjsLib from 'pdfjs-dist';
import {
ChevronLeftIcon,
@@ -14,11 +16,16 @@ import {
ExpandIcon,
MinimizeIcon
} from '@/components/common/Icons';
import { getCanvasRendererSettings } from './settings';
import { useSettings } from '@/hooks/useSettings';
import { useProperties } from '@/hooks/useProperties';
import type { RendererProps } from '@/plugins/PluginInterface';
import './styles.css';
import { getCanvasRendererSettings } from './settings';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs',
import.meta.url
).toString();
export interface CanvasRendererHandle {
updateSvgContent: (svgBuffer: ArrayBuffer) => void;
@@ -61,6 +68,8 @@ const CanvasRenderer: React.FC<RendererProps> = ({
const canvasRefs = useRef<Map<number, HTMLCanvasElement>>(new Map());
const propertiesRegistered = useRef(false);
const svgPagesRef = useRef<Map<number, string>>(new Map());
const pdfDocRef = useRef<any>(null);
const contentTypeRef = useRef<'svg' | 'pdf'>('svg');
const fullSvgBufferRef = useRef<ArrayBuffer | null>(null);
const pendingRenderRef = useRef<Set<number>>(new Set());
const renderingRef = useRef<Set<number>>(new Set());
@@ -97,6 +106,22 @@ const CanvasRenderer: React.FC<RendererProps> = ({
});
}, []);
const parsePdfPages = useCallback(async (pdfBuffer: ArrayBuffer): Promise<void> => {
const loadingTask = pdfjsLib.getDocument({ data: pdfBuffer });
const pdfDoc = await loadingTask.promise;
pdfDocRef.current = pdfDoc;
const metadata = new Map<number, { width: number; height: number }>();
for (let i = 1; i <= pdfDoc.numPages; i++) {
const page = await pdfDoc.getPage(i);
const viewport = page.getViewport({ scale: 1.0 });
metadata.set(i, { width: viewport.width, height: viewport.height });
}
setPageMetadata(metadata);
setNumPages(pdfDoc.numPages);
}, []);
const renderPageToCanvas = useCallback((pageNumber: number) => {
if (renderingRef.current.has(pageNumber)) {
pendingRenderRef.current.add(pageNumber);
@@ -166,6 +191,57 @@ const CanvasRenderer: React.FC<RendererProps> = ({
img.src = url;
}, [pageMetadata, scale]);
const renderPdfPageToCanvas = useCallback(async (pageNumber: number) => {
if (!pdfDocRef.current || renderingRef.current.has(pageNumber)) {
if (renderingRef.current.has(pageNumber)) {
pendingRenderRef.current.add(pageNumber);
}
return;
}
const canvas = canvasRefs.current.get(pageNumber);
if (!canvas) return;
renderingRef.current.add(pageNumber);
try {
const page = await pdfDocRef.current.getPage(pageNumber);
const viewport = page.getViewport({ scale });
const ctx = canvas.getContext('2d');
if (!ctx) {
renderingRef.current.delete(pageNumber);
return;
}
const pixelRatio = Math.min(window.devicePixelRatio || 1, 2);
const scaledViewport = page.getViewport({ scale: scale * pixelRatio });
canvas.width = scaledViewport.width;
canvas.height = scaledViewport.height;
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
const renderContext = {
canvasContext: ctx,
viewport: scaledViewport
};
await page.render(renderContext).promise;
renderingRef.current.delete(pageNumber);
if (pendingRenderRef.current.has(pageNumber)) {
pendingRenderRef.current.delete(pageNumber);
requestAnimationFrame(() => renderPdfPageToCanvas(pageNumber));
}
} catch (error) {
console.error(`Failed to render PDF page ${pageNumber}:`, error);
renderingRef.current.delete(pageNumber);
pendingRenderRef.current.delete(pageNumber);
}
}, [scale]);
const getPageHeight = useCallback((pageNum: number): number => {
const meta = pageMetadata.get(pageNum);
const baseHeight = meta?.height || 842;
@@ -266,14 +342,22 @@ const CanvasRenderer: React.FC<RendererProps> = ({
const renderVisiblePages = useCallback(() => {
if (scrollView) {
for (let i = renderRange.start; i <= renderRange.end; i++) {
if (i <= numPages && svgPagesRef.current.has(i)) {
renderPageToCanvas(i);
if (i <= numPages) {
if (contentTypeRef.current === 'svg' && svgPagesRef.current.has(i)) {
renderPageToCanvas(i);
} else if (contentTypeRef.current === 'pdf' && pdfDocRef.current) {
renderPdfPageToCanvas(i);
}
}
}
} else if (svgPagesRef.current.has(currentPage)) {
renderPageToCanvas(currentPage);
} else {
if (contentTypeRef.current === 'svg' && svgPagesRef.current.has(currentPage)) {
renderPageToCanvas(currentPage);
} else if (contentTypeRef.current === 'pdf' && pdfDocRef.current) {
renderPdfPageToCanvas(currentPage);
}
}
}, [scrollView, renderRange, currentPage, numPages, renderPageToCanvas]);
}, [scrollView, renderRange, currentPage, numPages, renderPageToCanvas, renderPdfPageToCanvas]);
useEffect(() => {
if (!scrollView || !scrollContainerRef.current) return;
@@ -323,17 +407,25 @@ const CanvasRenderer: React.FC<RendererProps> = ({
return () => clearTimeout(timer);
}, [scale, scrollView, updateCurrentPageFromScroll]);
const updateSvgContent = useCallback(async (svgBuffer: ArrayBuffer) => {
if (!svgBuffer || svgBuffer.byteLength === 0) return;
const updateContent = useCallback(async (buffer: ArrayBuffer) => {
if (!buffer || buffer.byteLength === 0) return;
fullSvgBufferRef.current = svgBuffer;
fullSvgBufferRef.current = buffer;
const arr = new Uint8Array(buffer);
const isPdf = arr.length > 4 && arr[0] === 0x25 && arr[1] === 0x50 && arr[2] === 0x44 && arr[3] === 0x46;
contentTypeRef.current = isPdf ? 'pdf' : 'svg';
try {
const { pages, metadata } = await parseSvgPages(svgBuffer);
if (isPdf) {
await parsePdfPages(buffer);
} else {
const { pages, metadata } = await parseSvgPages(buffer);
svgPagesRef.current = pages;
setPageMetadata(metadata);
setNumPages(pages.size);
}
svgPagesRef.current = pages;
setPageMetadata(metadata);
setNumPages(pages.size);
setIsLoading(false);
setError(null);
@@ -342,11 +434,11 @@ const CanvasRenderer: React.FC<RendererProps> = ({
});
} catch (err) {
console.error('[CanvasRenderer] Failed to parse SVG:', err);
setError(`Failed to parse SVG: ${err}`);
console.error('[CanvasRenderer] Failed to parse content:', err);
setError(`Failed to parse content: ${err}`);
setIsLoading(false);
}
}, [parseSvgPages, renderVisiblePages]);
}, [parseSvgPages, parsePdfPages, renderVisiblePages]);
useEffect(() => {
if (controllerRef) {
@@ -355,7 +447,7 @@ const CanvasRenderer: React.FC<RendererProps> = ({
const buffer = typeof newContent === 'string'
? new TextEncoder().encode(newContent).buffer
: newContent;
updateSvgContent(buffer);
updateContent(buffer);
}
});
}
@@ -364,7 +456,7 @@ const CanvasRenderer: React.FC<RendererProps> = ({
controllerRef(null);
}
};
}, [controllerRef, updateSvgContent]);
}, [controllerRef, updateContent]);
useEffect(() => {
if (propertiesRegistered.current) return;
@@ -399,12 +491,12 @@ const CanvasRenderer: React.FC<RendererProps> = ({
useEffect(() => {
if (content && (content instanceof ArrayBuffer) && content.byteLength > 0) {
updateSvgContent(content);
updateContent(content);
}
}, []);
useEffect(() => {
if (numPages === 0 || svgPagesRef.current.size === 0) return;
if (numPages === 0) return;
renderVisiblePages();
}, [scale, renderVisiblePages, numPages]);
@@ -516,11 +608,13 @@ const CanvasRenderer: React.FC<RendererProps> = ({
if (onDownload && fileName) {
onDownload(fileName);
} else if (fullSvgBufferRef.current) {
const blob = new Blob([fullSvgBufferRef.current], { type: 'image/svg+xml' });
const mimeType = contentTypeRef.current === 'pdf' ? 'application/pdf' : 'image/svg+xml';
const extension = contentTypeRef.current === 'pdf' ? '.pdf' : '.svg';
const blob = new Blob([fullSvgBufferRef.current], { type: mimeType });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = fileName?.replace(/\.typ$/, '.svg') || 'output.svg';
a.download = fileName?.replace(/\.(typ|svg|pdf)$/i, extension) || `output${extension}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);

View File

@@ -1,3 +1,6 @@
/// <reference lib="webworker" />
export { };
interface ParseMessage {
type: 'parse';
svgBuffer: ArrayBuffer;

View File

@@ -1,7 +1,7 @@
{
"name": "texlyre",
"private": true,
"version": "0.4.22",
"version": "0.4.23",
"description": "A local-first LaTeX & Typst collaborative web editor ",
"author": "Fares Abawi <fares@abawi.me> (https://abawi.me)",
"license": "AGPL-3.0-or-later",

View File

@@ -1,5 +1,5 @@
// These constants are automatically generated. Do not edit directly. **
const CACHE_NAME = `texlyre-v0.4.22`; //`texlyre-v${process.env.npm_package_version || '1'}`;
const CACHE_NAME = `texlyre-v0.4.23`; //`texlyre-v${process.env.npm_package_version || '1'}`;
const BASE_PATH = '/texlyre/';
// *** End automatic generation ***

View File

@@ -48,6 +48,7 @@ import KeyboardShortcutsModal from '../common/KeyboardShortcutsModal';
import PrivacyModal from '../common/PrivacyModal';
import GuestUpgradeBanner from '../auth/GuestUpgradeBanner';
import GuestUpgradeModal from '../auth/GuestUpgradeModal';
import { TypstOutputFormat } from '../../types/typst';
interface EditorAppProps {
docUrl: YjsDocUrl;
@@ -105,7 +106,7 @@ const EditorAppView: React.FC<EditorAppProps> = ({
mainFile: undefined as string | undefined,
latexEngine: undefined as ('pdftex' | 'xetex' | 'luatex') | undefined,
typstEngine: undefined as string | undefined,
typstOutputFormat: undefined as ('pdf' | 'svg' | 'canvas') | undefined
typstOutputFormat: undefined as (TypstOutputFormat) | undefined
});
const { isCompiling, triggerAutoCompile } = useLaTeX();
const { isCompiling: isTypstCompiling, triggerAutoCompile: triggerTypstAutoCompile } = useTypst();

View File

@@ -111,7 +111,7 @@ const TypesetterInfo: React.FC<TypesetterInfoProps> = ({ type }) => {
<strong>{t('Output Formats:')}</strong>
<ul>
<li>{t('PDF')}</li>
<li>{t('Canvas (SVG)')}</li>
<li>{t('SVG')}</li>
</ul>
</div>
</>);

View File

@@ -536,6 +536,7 @@ const TypstCompileButton: React.FC<TypstCompileButtonProps> = ({
className="dropdown-select"
disabled={isCompiling}>
<option value="pdf">{t('PDF')}</option>
<option value="canvas-pdf">{t('Canvas (PDF)')}</option>
<option value="canvas">{t('Canvas (SVG)')}</option>
</select>
{effectiveFormat === 'pdf' &&

View File

@@ -1,4 +1,3 @@
// src/components/output/TypstOutput.tsx
import { t } from '@/i18n';
import React from 'react';
import { useEffect, useRef, useState, useMemo, useCallback } from 'react';
@@ -12,6 +11,7 @@ import type { RendererController } from '../../plugins/PluginInterface'
import ResizablePanel from '../common/ResizablePanel';
import TypstCompileButton from './TypstCompileButton';
import { isTypstFile, toArrayBuffer } from '../../utils/fileUtils';
import { TypstOutputFormat } from '../../types/typst';
interface TypstOutputProps {
className?: string;
@@ -55,13 +55,14 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
const [visualizerHeight, setVisualizerHeight] = useState(300);
const [visualizerCollapsed, setVisualizerCollapsed] = useState(false);
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const canvasControllerRef = useRef<RendererController | null>(null);
const useEnhancedRenderer = getSetting('pdf-renderer-enable')?.value ?? true;
const loggerPlugin = pluginRegistry.getLoggerForType('typst');
useEffect(() => {
if (compiledCanvas && currentFormat === 'canvas' && canvasControllerRef.current?.updateContent) {
if (compiledCanvas && (currentFormat === 'canvas' || currentFormat === 'canvas-pdf') && canvasControllerRef.current?.updateContent) {
canvasControllerRef.current.updateContent(compiledCanvas);
}
}, [compiledCanvas, currentFormat]);
@@ -136,7 +137,9 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
case 'pdf':
return compiledPdf;
case 'svg':
return compiledSvg;
case 'canvas':
case 'canvas-pdf':
return compiledCanvas;
default:
return null;
@@ -163,11 +166,14 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
let blob: Blob;
switch (format) {
case 'svg':
if (!compiledSvg) return;
blob = new Blob([compiledSvg], { type: 'image/svg+xml' });
break;
case 'pdf':
if (!compiledPdf) return;
blob = new Blob([toArrayBuffer(compiledPdf)], { type: 'application/pdf' });
break;
case 'svg':
case 'canvas':
if (!compiledCanvas) return;
blob = new Blob([toArrayBuffer(compiledCanvas)], { type: 'text/plain' });
@@ -186,7 +192,7 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
URL.revokeObjectURL(url);
}, [compiledPdf, compiledSvg, compiledCanvas]);
const handleTabSwitch = useCallback((format: 'pdf' | 'svg' | 'canvas') => {
const handleTabSwitch = useCallback((format: TypstOutputFormat) => {
if (currentFormat !== format) {
setCurrentFormat(format);
@@ -234,7 +240,7 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
}
if (currentFormat === 'canvas') {
if (currentFormat === 'canvas' || currentFormat === 'canvas-pdf') {
const canvasRenderer = pluginRegistry.getRendererForOutput('canvas', 'canvas-renderer');
return (
@@ -242,8 +248,8 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
{canvasRenderer ?
React.createElement(canvasRenderer.renderOutput, {
content: compiledCanvas || new ArrayBuffer(0),
mimeType: 'image/svg+xml',
fileName: 'output.svg',
mimeType: currentFormat === 'canvas-pdf' ? 'application/pdf' : 'image/svg+xml',
fileName: currentFormat === 'canvas-pdf' ? 'output.pdf' : 'output.svg',
onDownload: (fileName) => handleSaveOutput('canvas', fileName),
controllerRef: (controller) => { canvasControllerRef.current = controller; }
}) :
@@ -274,11 +280,14 @@ const TypstOutput: React.FC<TypstOutputProps> = ({
className={`tab-button ${currentView === 'output' && currentFormat === 'pdf' ? 'active' : ''}`}
onClick={() => handleTabSwitch('pdf')}>{t('PDF')}
</button>
<button
className={`tab-button ${currentView === 'output' && currentFormat === 'canvas-pdf' ? 'active' : ''}`}
onClick={() => handleTabSwitch('canvas-pdf')}>{t('Canvas (PDF)')}
</button>
<button
className={`tab-button ${currentView === 'output' && currentFormat === 'canvas' ? 'active' : ''}`}
onClick={() => handleTabSwitch('canvas')}>{t('Canvas')}
onClick={() => handleTabSwitch('canvas')}>{t('Canvas (SVG)')}
</button>
</>
}

View File

@@ -87,8 +87,9 @@ export const TypstProvider: React.FC<TypstProviderProps> = ({ children }) => {
defaultValue: initialDefaultFormat,
options: [
{ label: t("PDF"), value: 'pdf' },
{ label: t("Canvas (SVG)"), value: 'canvas' }],
{ label: t("Canvas (PDF)"), value: 'canvas-pdf' },
{ label: t("Canvas (SVG)"), value: 'canvas' }
],
onChange: (value) => {
setCurrentFormat(value as TypstOutputFormat);
typstService.setDefaultFormat(value as TypstOutputFormat);
@@ -193,6 +194,12 @@ export const TypstProvider: React.FC<TypstProviderProps> = ({ children }) => {
console.error('[TypstContext] result.canvas is null/undefined!');
}
break;
case 'canvas-pdf':
if (result.canvas) {
setCompiledCanvas(result.canvas);
setCurrentView('output');
}
break;
}
} else {
setCompileError(t('Compilation failed. Check the log in the main window.'));

View File

@@ -1,9 +1,9 @@
// src/extensions/typst.ts/TypstCompilerEngine.ts
import { nanoid } from 'nanoid';
import type { TypstPdfOptions } from '../../types/typst';
import type { TypstOutputFormat, TypstPdfOptions } from '../../types/typst';
export type TypstWorkerMessage =
| { id: string; type: 'compile'; payload: { mainFilePath: string; sources: Record<string, string | Uint8Array>; format: 'pdf' | 'svg' | 'canvas'; pdfOptions?: TypstPdfOptions } }
| { id: string; type: 'compile'; payload: { mainFilePath: string; sources: Record<string, string | Uint8Array>; format: TypstOutputFormat; pdfOptions?: TypstPdfOptions } }
| { id: string; type: 'ping' };
export type TypstWorkerResponse =
@@ -58,7 +58,7 @@ export class TypstCompilerEngine {
async compile(
mainFilePath: string,
sources: Record<string, string | Uint8Array>,
format: 'pdf' | 'svg' | 'canvas',
format: TypstOutputFormat,
pdfOptions?: TypstPdfOptions,
signal?: AbortSignal
): Promise<{ format: string; output: Uint8Array | string; diagnostics?: any[] }> {
@@ -77,7 +77,7 @@ export class TypstCompilerEngine {
private callWorker<TType extends 'compile' | 'ping'>(
type: TType,
payload: TType extends 'compile'
? { mainFilePath: string; sources: Record<string, string | Uint8Array>; format: 'pdf' | 'svg' | 'canvas'; pdfOptions?: TypstPdfOptions }
? { mainFilePath: string; sources: Record<string, string | Uint8Array>; format: TypstOutputFormat; pdfOptions?: TypstPdfOptions }
: undefined,
signal?: AbortSignal
): Promise<any> {

View File

@@ -3,12 +3,13 @@ export { };
import { createTypstCompiler } from '@myriaddreamin/typst.ts/compiler';
import { createTypstRenderer } from '@myriaddreamin/typst.ts/renderer';
import { TypstSnippet } from '@myriaddreamin/typst.ts/dist/esm/contrib/snippet.mjs';
import { TypstOutputFormat } from '@/types/typst';
const BASE_PATH = __BASE_PATH__;
declare const self: DedicatedWorkerGlobalScope;
type OutputFormat = 'pdf' | 'svg' | 'canvas';
type OutputFormat = TypstOutputFormat;
type CompileMessage = {
id: string;
@@ -191,7 +192,7 @@ self.addEventListener('message', async (e: MessageEvent<InboundMessage>) => {
let output: Uint8Array | string;
let diagnostics: any[] = [];
if (format === 'pdf') {
if (format === 'pdf' || format === 'canvas-pdf') {
const pdfStandard = pdfOptions?.pdfStandard || '"1.7"';
const pdfTags = pdfOptions?.pdfTags !== undefined ? pdfOptions.pdfTags : true;
const creationTimestamp = pdfOptions?.creationTimestamp || Math.floor(Date.now() / 1000);

View File

@@ -2,7 +2,7 @@
import type { User } from '../types/auth';
import type { FileNode } from '../types/files';
import type { Project } from '../types/projects';
import type { TypstPdfOptions } from '../types/typst';
import type { TypstPdfOptions, TypstOutputFormat } from '../types/typst';
export interface UnifiedManifest {
version: string;
@@ -18,7 +18,7 @@ export interface ProjectMetadata {
mainFile?: string;
latexEngine?: 'pdftex' | 'xetex' | 'luatex';
typstEngine?: string;
typstOutputFormat?: 'pdf' | 'svg' | 'canvas';
typstOutputFormat?: TypstOutputFormat;
typstPdfOptions?: TypstPdfOptions;
docUrl: string;
createdAt: number;

View File

@@ -204,7 +204,7 @@ class TypstService {
const baseName = this.getBaseName(normalizedMainFileName);
const files: Array<{ content: Uint8Array; name: string; mimeType: string }> = [];
if (format === 'pdf' && output instanceof Uint8Array) {
if ((format === 'pdf' || format === 'canvas-pdf') && output instanceof Uint8Array) {
files.push({
content: output,
name: `${baseName}.pdf`,
@@ -248,9 +248,10 @@ class TypstService {
}
getSupportedFormats(): TypstOutputFormat[] {
return ['pdf', 'svg', 'canvas'];
return ['pdf', 'svg', 'canvas', 'canvas-pdf'];
}
setDefaultFormat(format: TypstOutputFormat): void {
this.defaultFormat = format;
}
@@ -332,7 +333,9 @@ class TypstService {
case 'canvas':
console.log('[TypstService] Creating canvas result, encoding string to Uint8Array');
result.canvas = new TextEncoder().encode(output as string);
console.log('[TypstService] Canvas result created', { canvasLength: result.canvas.length });
break;
case 'canvas-pdf':
result.canvas = output as Uint8Array;
break;
}

View File

@@ -1,6 +1,6 @@
// src/types/documents.ts
import type { ChatMessage } from './chat';
import type { TypstPdfOptions } from './typst';
import type { TypstPdfOptions, TypstOutputFormat } from './typst';
export interface Document {
id: string;
@@ -20,7 +20,7 @@ export interface DocumentList {
mainFile?: string;
latexEngine?: 'pdftex' | 'xetex' | 'luatex';
typstEngine?: string;
typstOutputFormat?: 'pdf' | 'svg' | 'canvas';
typstOutputFormat?: TypstOutputFormat;
latexAutoCompileOnSave?: boolean;
typstAutoCompileOnSave?: boolean;
typstPdfOptions?: TypstPdfOptions;

View File

@@ -1,6 +1,6 @@
// src/types/typst.ts
export type TypstOutputFormat = 'pdf' | 'svg' | 'canvas';
export type TypstOutputFormat = 'pdf' | 'svg' | 'canvas' | 'canvas-pdf';
export interface TypstPdfOptions {
pdfStandard?: string;

View File

@@ -1,5 +1,5 @@
// This file is automatically generated. Do not edit directly.
// Generated on: 2025-12-12T10:12:05.399Z
// Generated on: 2025-12-12T13:02:56.095Z
// Settings found: 157
// Properties found: 53

View File

@@ -1,7 +1,7 @@
{
"_meta": {
"lastUpdated": "2025-12-12T10:12:07.103Z",
"totalKeys": 1682
"lastUpdated": "2025-12-12T13:02:57.657Z",
"totalKeys": 1683
},
"languages": [
{
@@ -10,7 +10,7 @@
"nativeName": "English",
"direction": "ltr",
"coverage": 100,
"totalKeys": 1682,
"totalKeys": 1683,
"translatedKeys": 0,
"filePath": "locales/en.json"
},
@@ -20,7 +20,7 @@
"nativeName": "العربية",
"direction": "rtl",
"coverage": 95,
"totalKeys": 1682,
"totalKeys": 1683,
"translatedKeys": 1595,
"filePath": "locales/ar.json"
},
@@ -30,7 +30,7 @@
"nativeName": "Deutsch",
"direction": "ltr",
"coverage": 92,
"totalKeys": 1682,
"totalKeys": 1683,
"translatedKeys": 1543,
"filePath": "locales/de.json"
}

View File

@@ -302,6 +302,7 @@
"Cannot rename": "لا يمكن إعادة التسمية",
"Canvas": "Canvas",
"Canvas (SVG)": "Canvas (SVG)",
"Canvas (PDF)": "Canvas (PDF)",
"Canvas Output": "إخراج Canvas",
"Canvas renderer is disabled. Please enable it in settings.": "مُصَيِّر Canvas معطل. يرجى تفعيله في الإعدادات.",
"Canvas renderer not available": "مُصَيِّر Canvas غير متاح",

View File

@@ -258,6 +258,7 @@
"Cannot rename": "Kann nicht umbenennen",
"Canvas": "Canvas",
"Canvas (SVG)": "Canvas (SVG)",
"Canvas (PDF)": "Canvas (PDF)",
"Canvas Output": "Canvas-Ausgabe",
"Canvas renderer is disabled. Please enable it in settings.": "Leinenrenderer ist deaktiviert. Bitte aktivieren Sie ihn in den Einstellungen.",
"Canvas renderer not available": "Canvas Renderer nicht verfügbar",

View File

@@ -259,6 +259,7 @@
"Cannot rename": "Cannot rename",
"Canvas": "Canvas",
"Canvas (SVG)": "Canvas (SVG)",
"Canvas (PDF)": "Canvas (PDF)",
"Canvas Output": "Canvas Output",
"Canvas renderer is disabled. Please enable it in settings.": "Canvas renderer is disabled. Please enable it in settings.",
"Canvas renderer not available": "Canvas renderer not available",

View File

@@ -38,6 +38,7 @@
"include": [
"src/**/*",
"scripts/**/*",
"extras/**/*",
"build.ts",
"plugins.config.js",
"texlyre.config.ts"