fix: 完善本地与网络图片渲染
This commit is contained in:
+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>
|
||||
|
||||
Reference in New Issue
Block a user