fix: 完善本地与网络图片渲染
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
# Desktop 桌面端
|
||||
|
||||
`apps/desktop` 是 `v0.4.0` 新增的 Electron 桌面应用。当前已经完成
|
||||
Electron 壳、受限 preload、进程内应用服务、隐藏分页窗口、Electron
|
||||
Chromium PDF 输出、原生 PDF 另存为和 Windows 安装包验证。
|
||||
Electron 壳、受限 preload、进程内应用服务、原生 Markdown 文件选择、
|
||||
同目录图片解析、隐藏分页窗口、Electron Chromium PDF 输出、原生 PDF
|
||||
另存为和 Windows 安装包验证。
|
||||
|
||||
## 开发
|
||||
|
||||
@@ -26,7 +27,7 @@ npm run make -w @md-to-pdf/desktop
|
||||
```
|
||||
|
||||
版本化目录包、Squirrel `Setup.exe` 和 ZIP 输出到
|
||||
`apps/desktop/out/v0.4.0/`。该目录被 Git 忽略。公司内部分发以
|
||||
`apps/desktop/out/v0.4.1/`。该目录被 Git 忽略。公司内部分发以
|
||||
`Setup.exe` 为正式安装包,ZIP 作为免安装辅助包;当前未配置代码签名,
|
||||
Windows 首次运行可能显示“未知发布者”提示。
|
||||
|
||||
@@ -42,7 +43,10 @@ Web 静态资源作为只读资源放在 Electron `resources/dist` 下,生产
|
||||
|
||||
- 应用窗口和 PDF 窗口均关闭 Node Integration;
|
||||
- 启用 Context Isolation 和 Chromium Sandbox;
|
||||
- preload 只暴露渲染、主题、生成 PDF 和保存 PDF 所需的窄接口;
|
||||
- preload 只暴露打开 Markdown、渲染、主题、生成 PDF 和保存 PDF 所需
|
||||
的窄接口;
|
||||
- 本地图片只能从当前 Markdown 所在目录内读取,并拒绝路径穿越和符号
|
||||
链接越界;
|
||||
- 主进程校验 IPC 发送者和分页载荷;
|
||||
- PDF 窗口使用独立内存 Session,并阻止外部网络资源;
|
||||
- 应用页面使用自定义协议,不使用 `file://`。
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@md-to-pdf/desktop",
|
||||
"version": "0.4.0",
|
||||
"version": "0.4.1",
|
||||
"private": true,
|
||||
"productName": "Markdown PDF 导出器",
|
||||
"description": "Markdown PDF 导出器桌面端",
|
||||
@@ -8,7 +8,7 @@
|
||||
"type": "module",
|
||||
"main": "dist/main.js",
|
||||
"dependencies": {
|
||||
"@md-to-pdf/application": "0.4.0"
|
||||
"@md-to-pdf/application": "0.4.1"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run clean && npm run build:main && npm run build:preload",
|
||||
|
||||
@@ -5,11 +5,13 @@ import {
|
||||
DESKTOP_GET_THEME_CSS,
|
||||
DESKTOP_GENERATE_PDF,
|
||||
DESKTOP_LIST_THEMES,
|
||||
DESKTOP_OPEN_MARKDOWN,
|
||||
DESKTOP_RENDER_MARKDOWN,
|
||||
DESKTOP_SAVE_PDF
|
||||
} from "./channels.js";
|
||||
|
||||
contextBridge.exposeInMainWorld("mdToPdfDesktop", {
|
||||
openMarkdown: () => ipcRenderer.invoke(DESKTOP_OPEN_MARKDOWN),
|
||||
renderMarkdown: (input: MarkdownRenderRequest) =>
|
||||
ipcRenderer.invoke(DESKTOP_RENDER_MARKDOWN, input),
|
||||
listThemes: () => ipcRenderer.invoke(DESKTOP_LIST_THEMES),
|
||||
|
||||
@@ -12,7 +12,8 @@ export function parseMarkdownRenderRequest(
|
||||
}
|
||||
return {
|
||||
markdown: value.markdown,
|
||||
language: value.language
|
||||
language: value.language,
|
||||
resources: value.resources
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
export const DESKTOP_RENDER_MARKDOWN =
|
||||
"md-to-pdf:desktop:render-markdown";
|
||||
export const DESKTOP_OPEN_MARKDOWN =
|
||||
"md-to-pdf:desktop:open-markdown";
|
||||
export const DESKTOP_LIST_THEMES =
|
||||
"md-to-pdf:desktop:list-themes";
|
||||
export const DESKTOP_GET_THEME_CSS =
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
session,
|
||||
type IpcMainInvokeEvent
|
||||
} from "electron";
|
||||
import { access, writeFile } from "node:fs/promises";
|
||||
import { access, readFile, writeFile } from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import {
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
DESKTOP_GET_THEME_CSS,
|
||||
DESKTOP_GENERATE_PDF,
|
||||
DESKTOP_LIST_THEMES,
|
||||
DESKTOP_OPEN_MARKDOWN,
|
||||
DESKTOP_RENDER_MARKDOWN,
|
||||
DESKTOP_SAVE_PDF
|
||||
} from "./channels.js";
|
||||
@@ -229,7 +230,31 @@ async function createApplication(
|
||||
event.senderFrame !== null &&
|
||||
new URL(event.senderFrame.url).origin ===
|
||||
new URL(applicationUrl).origin;
|
||||
let documentRoot: string | undefined;
|
||||
|
||||
ipcMain.handle(DESKTOP_OPEN_MARKDOWN, async (event) => {
|
||||
if (!validateSender(event)) {
|
||||
throw new Error("拒绝未授权的桌面文件请求");
|
||||
}
|
||||
const selection = await dialog.showOpenDialog(window, {
|
||||
title: "打开 Markdown",
|
||||
properties: ["openFile"],
|
||||
filters: [
|
||||
{ name: "Markdown 文件", extensions: ["md", "markdown"] },
|
||||
{ name: "文本文件", extensions: ["txt"] }
|
||||
]
|
||||
});
|
||||
const filePath = selection.filePaths[0];
|
||||
if (selection.canceled || !filePath) {
|
||||
return undefined;
|
||||
}
|
||||
const content = await readFile(filePath, "utf8");
|
||||
documentRoot = path.dirname(filePath);
|
||||
return {
|
||||
markdown: content,
|
||||
fileName: path.basename(filePath)
|
||||
};
|
||||
});
|
||||
ipcMain.handle(
|
||||
DESKTOP_RENDER_MARKDOWN,
|
||||
async (event, unsafeRequest: unknown) => {
|
||||
@@ -237,7 +262,8 @@ async function createApplication(
|
||||
throw new Error("拒绝未授权的桌面渲染请求");
|
||||
}
|
||||
return applicationService.render(
|
||||
parseMarkdownRenderRequest(unsafeRequest)
|
||||
parseMarkdownRenderRequest(unsafeRequest),
|
||||
documentRoot ? { localRoot: documentRoot } : {}
|
||||
);
|
||||
}
|
||||
);
|
||||
@@ -317,6 +343,7 @@ async function createApplication(
|
||||
|
||||
window.once("ready-to-show", () => window.show());
|
||||
window.on("closed", () => {
|
||||
ipcMain.removeHandler(DESKTOP_OPEN_MARKDOWN);
|
||||
ipcMain.removeHandler(DESKTOP_RENDER_MARKDOWN);
|
||||
ipcMain.removeHandler(DESKTOP_LIST_THEMES);
|
||||
ipcMain.removeHandler(DESKTOP_GET_THEME_CSS);
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
"typecheck": "tsc -p tsconfig.json --noEmit --pretty false"
|
||||
},
|
||||
"dependencies": {
|
||||
"@md-to-pdf/application": "0.4.0",
|
||||
"@md-to-pdf/application": "0.4.1",
|
||||
"@md-to-pdf/core": "0.1.0",
|
||||
"@md-to-pdf/renderer": "0.1.0",
|
||||
"fastify": "^5.6.2",
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
interface RenderRequestBody {
|
||||
markdown?: unknown;
|
||||
language?: unknown;
|
||||
resources?: unknown;
|
||||
}
|
||||
|
||||
interface PdfRequestBody extends RenderRequestBody {
|
||||
@@ -108,7 +109,7 @@ function createPdfServerTiming(
|
||||
export function buildApp(options: BuildAppOptions = {}) {
|
||||
const app = Fastify({
|
||||
logger: options.logger ?? true,
|
||||
bodyLimit: 2 * 1024 * 1024
|
||||
bodyLimit: 24 * 1024 * 1024
|
||||
});
|
||||
const applicationService =
|
||||
options.applicationService ??
|
||||
@@ -214,7 +215,7 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
"/api/render",
|
||||
async (request, reply) => {
|
||||
try {
|
||||
return applicationService.render(request.body ?? {});
|
||||
return await applicationService.render(request.body ?? {});
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationRequestError) {
|
||||
return reply.code(error.statusCode).send({
|
||||
@@ -267,7 +268,7 @@ export function buildApp(options: BuildAppOptions = {}) {
|
||||
const markdownStartedAt = performance.now();
|
||||
let rendered;
|
||||
try {
|
||||
rendered = applicationService.render(request.body ?? {});
|
||||
rendered = await applicationService.render(request.body ?? {});
|
||||
} catch (error) {
|
||||
if (error instanceof ApplicationRequestError) {
|
||||
return reply.code(error.statusCode).send({
|
||||
|
||||
+115
-9
@@ -48,6 +48,9 @@ import {
|
||||
savePreviewZoom
|
||||
} from "./preview-zoom";
|
||||
import { useMarkdownRender } from "./use-markdown-render";
|
||||
import {
|
||||
type MarkdownImageResource
|
||||
} from "./application-backend";
|
||||
import {
|
||||
type ThemeSummary,
|
||||
useThemeResources
|
||||
@@ -59,6 +62,30 @@ const PrecisePdfPreview = lazy(async () => {
|
||||
});
|
||||
|
||||
type PreviewMode = "quick" | "precise";
|
||||
const imageFilePattern = /\.(?:avif|gif|jpe?g|png|svg|webp)$/iu;
|
||||
|
||||
function bytesToBase64(bytes: Uint8Array) {
|
||||
const parts: string[] = [];
|
||||
const chunkSize = 32_768;
|
||||
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
||||
parts.push(
|
||||
String.fromCharCode(...bytes.subarray(offset, offset + chunkSize))
|
||||
);
|
||||
}
|
||||
return window.btoa(parts.join(""));
|
||||
}
|
||||
|
||||
async function readImageResources(files: FileList) {
|
||||
const images = Array.from(files).filter((file) =>
|
||||
imageFilePattern.test(file.name)
|
||||
);
|
||||
return Promise.all(
|
||||
images.map(async (file): Promise<MarkdownImageResource> => ({
|
||||
path: file.webkitRelativePath || file.name,
|
||||
data: bytesToBase64(new Uint8Array(await file.arrayBuffer()))
|
||||
}))
|
||||
);
|
||||
}
|
||||
|
||||
const sampleMarkdown = `---
|
||||
title: Markdown PDF 示例
|
||||
@@ -86,6 +113,19 @@ keywords:
|
||||
|
||||
行内公式:$E = mc^2$
|
||||
|
||||
## 网络图片
|
||||
|
||||
### 中等尺寸
|
||||
|
||||

|
||||
|
||||
### 超高尺寸
|
||||
|
||||
下图用于验证图片高度超过单页内容区时,会等比例缩放并作为完整块放置,
|
||||
不会跨页切割。
|
||||
|
||||

|
||||
|
||||
\`\`\`mermaid
|
||||
flowchart LR
|
||||
A["Markdown"] --> B["统一 HTML"]
|
||||
@@ -346,6 +386,9 @@ option:
|
||||
export function App() {
|
||||
const [markdown, setMarkdown] = useState(sampleMarkdown);
|
||||
const [fileName, setFileName] = useState("示例文档.md");
|
||||
const [imageResources, setImageResources] = useState<
|
||||
MarkdownImageResource[]
|
||||
>([]);
|
||||
const [exportConfig, setExportConfig] =
|
||||
useState<ExportConfig>(loadExportConfig);
|
||||
const [settingsOpen, setSettingsOpen] = useState(false);
|
||||
@@ -407,6 +450,7 @@ export function App() {
|
||||
const { error: renderError, result } = useMarkdownRender(
|
||||
markdown,
|
||||
"zh-CN",
|
||||
imageResources,
|
||||
{
|
||||
onRenderStart: handleRenderStart,
|
||||
onStatusChange: setStatus
|
||||
@@ -469,9 +513,10 @@ export function App() {
|
||||
markdown,
|
||||
fileName,
|
||||
language: "zh-CN",
|
||||
resources: imageResources,
|
||||
exportConfig
|
||||
}),
|
||||
[exportConfig, fileName, markdown]
|
||||
[exportConfig, fileName, imageResources, markdown]
|
||||
);
|
||||
const pdfCacheKey = useMemo(
|
||||
() => createPdfExportCacheKey(pdfRequest),
|
||||
@@ -813,6 +858,8 @@ export function App() {
|
||||
setAppError("");
|
||||
setMarkdown(await file.text());
|
||||
setFileName(file.name);
|
||||
setImageResources([]);
|
||||
setStatus("Markdown 已载入;如含相对图片,请添加素材目录");
|
||||
} catch {
|
||||
setAppError("无法读取所选 Markdown 文件");
|
||||
} finally {
|
||||
@@ -820,6 +867,43 @@ export function App() {
|
||||
}
|
||||
}
|
||||
|
||||
async function handleAssetDirectoryChange(
|
||||
event: ChangeEvent<HTMLInputElement>
|
||||
) {
|
||||
const files = event.target.files;
|
||||
if (!files?.length) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
setAppError("");
|
||||
const resources = await readImageResources(files);
|
||||
setImageResources(resources);
|
||||
setStatus(`已载入 ${resources.length} 个图片素材`);
|
||||
} catch {
|
||||
setAppError("无法读取所选图片素材目录");
|
||||
} finally {
|
||||
event.target.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDesktopOpenMarkdown() {
|
||||
try {
|
||||
setAppError("");
|
||||
const opened = await window.mdToPdfDesktop?.openMarkdown();
|
||||
if (!opened) {
|
||||
return;
|
||||
}
|
||||
setMarkdown(opened.markdown);
|
||||
setFileName(opened.fileName);
|
||||
setImageResources([]);
|
||||
setStatus("Markdown 与同目录图片素材已载入");
|
||||
} catch (reason) {
|
||||
setAppError(
|
||||
reason instanceof Error ? reason.message : "无法打开 Markdown 文件"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
async function generatePdf(download: boolean) {
|
||||
if (exportingPdf) {
|
||||
return;
|
||||
@@ -920,14 +1004,36 @@ export function App() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="topbar-actions">
|
||||
<label className="file-button">
|
||||
选择 Markdown
|
||||
<input
|
||||
type="file"
|
||||
accept=".md,.markdown,text/markdown,text/plain"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
{window.mdToPdfDesktop ? (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleDesktopOpenMarkdown()}
|
||||
>
|
||||
打开 Markdown
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<label className="file-button">
|
||||
选择 Markdown
|
||||
<input
|
||||
type="file"
|
||||
accept=".md,.markdown,text/markdown,text/plain"
|
||||
onChange={handleFileChange}
|
||||
/>
|
||||
</label>
|
||||
<label className="file-button">
|
||||
添加素材目录
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
multiple
|
||||
// React 尚未声明 Chromium 的目录选择属性。
|
||||
{...({ webkitdirectory: "" } as Record<string, string>)}
|
||||
onChange={handleAssetDirectoryChange}
|
||||
/>
|
||||
</label>
|
||||
</>
|
||||
)}
|
||||
<button type="button" onClick={() => setSettingsOpen(true)}>
|
||||
导出设置
|
||||
</button>
|
||||
|
||||
@@ -12,6 +12,12 @@ export interface ThemeSummary {
|
||||
export interface MarkdownRenderInput {
|
||||
markdown: string;
|
||||
language: string;
|
||||
resources?: MarkdownImageResource[];
|
||||
}
|
||||
|
||||
export interface MarkdownImageResource {
|
||||
path: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
function throwIfAborted(signal?: AbortSignal) {
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import type { ExportConfig } from "@md-to-pdf/core";
|
||||
import {
|
||||
calculateDiagramPageFit,
|
||||
getPageContentDimensions
|
||||
} from "./diagram-page-fit";
|
||||
|
||||
function getImageBlock(image: HTMLImageElement) {
|
||||
const parent = image.parentElement;
|
||||
if (parent?.classList.contains("md-document-image-block")) {
|
||||
return parent;
|
||||
}
|
||||
if (
|
||||
parent?.tagName === "P" &&
|
||||
Array.from(parent.childNodes).every(
|
||||
(node) =>
|
||||
node === image ||
|
||||
(node.nodeType === Node.TEXT_NODE && !node.textContent?.trim())
|
||||
)
|
||||
) {
|
||||
parent.classList.add("md-document-image-block");
|
||||
return parent;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function fitDocumentImagesToPage(
|
||||
root: ParentNode,
|
||||
config: ExportConfig
|
||||
) {
|
||||
const pageContent = getPageContentDimensions(config);
|
||||
const images = Array.from(
|
||||
root.querySelectorAll<HTMLImageElement>("img.md-document-image")
|
||||
);
|
||||
|
||||
for (const image of images) {
|
||||
const block = getImageBlock(image);
|
||||
const width = image.naturalWidth;
|
||||
const height = image.naturalHeight;
|
||||
if (width <= 0 || height <= 0) {
|
||||
continue;
|
||||
}
|
||||
const imageHeight = image.getBoundingClientRect().height;
|
||||
const blockHeight = block?.getBoundingClientRect().height ?? imageHeight;
|
||||
const nonImageHeight = Math.max(0, blockHeight - imageHeight);
|
||||
const fit = calculateDiagramPageFit(
|
||||
{ width, height },
|
||||
{
|
||||
width: pageContent.width,
|
||||
height: Math.max(0, pageContent.height - nonImageHeight)
|
||||
}
|
||||
);
|
||||
if (!fit.scaled) {
|
||||
continue;
|
||||
}
|
||||
if (block?.classList.contains("md-document-image-block")) {
|
||||
block.dataset.pageHeightFitted = "true";
|
||||
block.style.setProperty("margin-block", "0", "important");
|
||||
}
|
||||
image.dataset.pageHeightFitted = "true";
|
||||
image.style.setProperty("display", "block");
|
||||
image.style.setProperty("width", `${fit.width}px`, "important");
|
||||
image.style.setProperty("height", `${fit.height}px`, "important");
|
||||
image.style.setProperty("max-width", "100%", "important");
|
||||
image.style.setProperty(
|
||||
"max-height",
|
||||
`${fit.height}px`,
|
||||
"important"
|
||||
);
|
||||
image.style.setProperty("object-fit", "contain");
|
||||
image.style.setProperty("margin-inline", "auto");
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import type {
|
||||
} from "@md-to-pdf/core";
|
||||
import { Previewer } from "pagedjs";
|
||||
import { fitOversizedEChartsToPage } from "./echarts-page-fit";
|
||||
import { fitDocumentImagesToPage } from "./document-image-fit";
|
||||
import { createMermaidSiteConfig } from "./mermaid-config";
|
||||
import {
|
||||
fitOversizedMermaidToPage,
|
||||
@@ -326,7 +327,8 @@ export class PagedDocumentRuntime {
|
||||
let mermaidConversionMs = 0;
|
||||
if (
|
||||
payload.features.includes("echarts") ||
|
||||
content.querySelector(".mermaid svg")
|
||||
content.querySelector(".mermaid svg") ||
|
||||
content.querySelector("img.md-document-image")
|
||||
) {
|
||||
const measurement = mountMeasurementContainer(
|
||||
documentRef,
|
||||
@@ -344,6 +346,10 @@ export class PagedDocumentRuntime {
|
||||
);
|
||||
echartsMs = performance.now() - echartsStartedAt;
|
||||
await waitForImages(measurement.host);
|
||||
fitDocumentImagesToPage(
|
||||
measurement.host,
|
||||
payload.exportConfig
|
||||
);
|
||||
|
||||
const echartsFitStartedAt = performance.now();
|
||||
fitOversizedEChartsToPage(
|
||||
|
||||
@@ -67,6 +67,25 @@ svg {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.md-document-image-block {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.md-document-image-block > .md-document-image {
|
||||
display: block;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.md-document-image-caption {
|
||||
margin-top: 0.55em;
|
||||
color: #64748b;
|
||||
font-size: 0.875em;
|
||||
line-height: 1.5;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
#write table {
|
||||
width: 100%;
|
||||
table-layout: auto;
|
||||
@@ -313,6 +332,11 @@ ${buildFooterCss(config)}
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
#write .md-document-image-block {
|
||||
break-inside: avoid;
|
||||
page-break-inside: avoid;
|
||||
}
|
||||
|
||||
${
|
||||
config.print.pageBreakBeforeH1
|
||||
? `#write h1:not(:first-child) {
|
||||
|
||||
@@ -2,11 +2,13 @@ import type {
|
||||
ExportConfig,
|
||||
PagedDocumentPayload
|
||||
} from "@md-to-pdf/core";
|
||||
import type { MarkdownImageResource } from "./application-backend";
|
||||
|
||||
export interface PdfExportRequest {
|
||||
markdown: string;
|
||||
fileName: string;
|
||||
language: string;
|
||||
resources: MarkdownImageResource[];
|
||||
exportConfig: ExportConfig;
|
||||
}
|
||||
|
||||
@@ -28,6 +30,7 @@ export function createPdfExportCacheKey(request: PdfExportRequest) {
|
||||
request.markdown,
|
||||
request.fileName,
|
||||
request.language,
|
||||
request.resources,
|
||||
request.exportConfig
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import type { RenderedMarkdownDocument } from "@md-to-pdf/core";
|
||||
import { renderMarkdownDocument } from "./application-backend";
|
||||
import type { MarkdownImageResource } from "./application-backend";
|
||||
|
||||
export interface MarkdownRenderCallbacks {
|
||||
onRenderStart?: () => void;
|
||||
@@ -10,6 +11,7 @@ export interface MarkdownRenderCallbacks {
|
||||
export function useMarkdownRender(
|
||||
markdown: string,
|
||||
language: string,
|
||||
resources: MarkdownImageResource[],
|
||||
callbacks: MarkdownRenderCallbacks = {}
|
||||
) {
|
||||
const [result, setResult] =
|
||||
@@ -25,7 +27,7 @@ export function useMarkdownRender(
|
||||
setError("");
|
||||
|
||||
void renderMarkdownDocument(
|
||||
{ markdown, language },
|
||||
{ markdown, language, resources },
|
||||
controller.signal
|
||||
)
|
||||
.then((payload) => {
|
||||
@@ -46,7 +48,13 @@ export function useMarkdownRender(
|
||||
window.clearTimeout(timer);
|
||||
controller.abort();
|
||||
};
|
||||
}, [language, markdown, onRenderStart, onStatusChange]);
|
||||
}, [
|
||||
language,
|
||||
markdown,
|
||||
onRenderStart,
|
||||
onStatusChange,
|
||||
resources
|
||||
]);
|
||||
|
||||
return { error, result };
|
||||
}
|
||||
|
||||
Vendored
+11
@@ -17,9 +17,20 @@ declare global {
|
||||
}
|
||||
|
||||
interface DesktopApplicationBridge {
|
||||
openMarkdown(): Promise<
|
||||
| {
|
||||
markdown: string;
|
||||
fileName: string;
|
||||
}
|
||||
| undefined
|
||||
>;
|
||||
renderMarkdown(input: {
|
||||
markdown: string;
|
||||
language: string;
|
||||
resources?: Array<{
|
||||
path: string;
|
||||
data: string;
|
||||
}>;
|
||||
}): Promise<RenderedMarkdownDocument>;
|
||||
listThemes(): Promise<{
|
||||
themes: Array<{
|
||||
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
|
||||
describe("应用版本", () => {
|
||||
it("从统一构建版本生成标题徽标", () => {
|
||||
expect(APP_VERSION).toBe("0.4.0");
|
||||
expect(APP_VERSION_LABEL).toBe("v0.4.0");
|
||||
expect(APP_VERSION).toBe("0.4.1");
|
||||
expect(APP_VERSION_LABEL).toBe("v0.4.1");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { defaultExportConfig } from "@md-to-pdf/core";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { fitDocumentImagesToPage } from "../src/document-image-fit";
|
||||
|
||||
describe("Markdown 图片单页适配", () => {
|
||||
it("标记独立图片段落并将超高图片缩放到单页", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML =
|
||||
'<p> <img class="md-document-image" src="data:image/png;base64,AA=="> </p>';
|
||||
const image = root.querySelector("img")!;
|
||||
Object.defineProperties(image, {
|
||||
naturalWidth: { value: 1000 },
|
||||
naturalHeight: { value: 3000 }
|
||||
});
|
||||
|
||||
fitDocumentImagesToPage(root, defaultExportConfig);
|
||||
|
||||
expect(image.parentElement?.classList).toContain(
|
||||
"md-document-image-block"
|
||||
);
|
||||
expect(image.dataset.pageHeightFitted).toBe("true");
|
||||
expect(image.parentElement?.dataset.pageHeightFitted).toBe("true");
|
||||
expect(
|
||||
image.parentElement?.style.getPropertyValue("margin-block")
|
||||
).toBe("0");
|
||||
expect(Number.parseFloat(image.style.height)).toBeCloseTo(
|
||||
1000.5748,
|
||||
3
|
||||
);
|
||||
expect(Number.parseFloat(image.style.width)).toBeCloseTo(
|
||||
333.5249,
|
||||
3
|
||||
);
|
||||
});
|
||||
|
||||
it("普通横图保持主题控制的尺寸", () => {
|
||||
const root = document.createElement("div");
|
||||
root.innerHTML =
|
||||
'<p><img class="md-document-image" src="data:image/png;base64,AA=="></p>';
|
||||
const image = root.querySelector("img")!;
|
||||
Object.defineProperties(image, {
|
||||
naturalWidth: { value: 1600 },
|
||||
naturalHeight: { value: 900 }
|
||||
});
|
||||
|
||||
fitDocumentImagesToPage(root, defaultExportConfig);
|
||||
|
||||
expect(image.dataset.pageHeightFitted).toBeUndefined();
|
||||
expect(image.style.height).toBe("");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user