mirror of
https://github.com/TeXlyre/texlyre.git
synced 2025-12-20 08:50:02 -06:00
WIP: Added canvas viewer for PDF as well
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/// <reference lib="webworker" />
|
||||
export { };
|
||||
|
||||
interface ParseMessage {
|
||||
type: 'parse';
|
||||
svgBuffer: ArrayBuffer;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 ***
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
</>);
|
||||
|
||||
@@ -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' &&
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -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.'));
|
||||
|
||||
@@ -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> {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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 غير متاح",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"include": [
|
||||
"src/**/*",
|
||||
"scripts/**/*",
|
||||
"extras/**/*",
|
||||
"build.ts",
|
||||
"plugins.config.js",
|
||||
"texlyre.config.ts"
|
||||
|
||||
Reference in New Issue
Block a user