fix: 完善本地与网络图片渲染

This commit is contained in:
SkyJourney
2026-07-27 22:12:16 +08:00
parent 06b30d084e
commit 32a37293d0
31 changed files with 1204 additions and 71 deletions
+115 -9
View File
@@ -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$
## 网络图片
### 中等尺寸
![NASA 地球中等尺寸示例](https://images-assets.nasa.gov/image/PIA18033/PIA18033~medium.jpg)
### 超高尺寸
下图用于验证图片高度超过单页内容区时,会等比例缩放并作为完整块放置,
不会跨页切割。
![超高网络图片分页示例](https://images.unsplash.com/photo-1500530855697-b586d89ba3ee?w=1200&q=80)
\`\`\`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>