@akairos/typeset-studio 0.2.1 → 0.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +91 -95
- package/dist/assets/index-3Ko1UCBU.css +1 -0
- package/dist/assets/index-C9J0f4Lf.js +317 -0
- package/dist/index.html +2 -2
- package/dist/lib/adapter.js +6 -7734
- package/dist/lib/canvas-renderer-cxnar75n.js +23 -0
- package/dist/lib/font-loader-j28mt43t.js +11 -0
- package/dist/lib/index-0370qz9e.js +303 -0
- package/dist/lib/index-37x76zdn.js +4 -0
- package/dist/lib/index-6c43c7h4.js +3880 -0
- package/dist/lib/index-t9w3p2p9.js +3366 -0
- package/dist/lib/index-zdhqg5f0.js +459 -0
- package/dist/lib/index.js +26 -7734
- package/dist/lib/render-xxd348m1.js +11 -0
- package/package.json +3 -7
- package/src/adapter.test.ts +117 -0
- package/src/adapter.ts +190 -281
- package/src/core/canvas-draw.ts +150 -47
- package/src/core/canvas-renderer.test.ts +58 -0
- package/src/core/canvas-renderer.ts +2 -1
- package/src/core/data-dir.ts +13 -0
- package/src/core/engine.ts +86 -56
- package/src/core/font-loader.test.ts +20 -0
- package/src/core/font-loader.ts +6 -2
- package/src/core/image-layout.ts +4 -0
- package/src/core/inline-text.ts +6 -4
- package/src/core/obstacle-flow.ts +29 -15
- package/src/core/page-images.test.ts +90 -0
- package/src/core/pretext.ts +4 -10
- package/src/core/template.test.ts +38 -1
- package/src/core/template.ts +53 -41
- package/src/core/types.ts +46 -0
- package/src/core/typography.ts +0 -14
- package/src/index.ts +6 -3
- package/src/server/auth.ts +18 -13
- package/src/server/index.ts +11 -16
- package/src/server/routes/fonts.ts +1 -1
- package/src/server/routes/render.ts +60 -17
- package/src/server/routes/slots.test.ts +96 -0
- package/src/server/routes/templates.ts +37 -48
- package/src/server/routes/upload.ts +2 -1
- package/src/web/App.tsx +15 -6
- package/src/web/components/CanvasViewport.tsx +56 -42
- package/src/web/components/EditorSidebar.tsx +59 -48
- package/src/web/components/Header.tsx +1 -1
- package/src/web/components/TemplateModal.tsx +25 -28
- package/src/web/hooks/image-layout.ts +31 -0
- package/src/web/hooks/useLocalFonts.ts +10 -1
- package/src/web/hooks/useTypeset.ts +139 -100
- package/data/templates/.gitkeep +0 -0
- package/data/templates/tpl_1791681833354_iofsk.json +0 -46
- package/data/templates/tpl_1791682603199_875ny.json +0 -66
- package/data/templates/tpl_1791682780929_qnuiy.json +0 -66
- package/data/templates/tpl_longform_paper_sage.json +0 -47
- package/data/templates/tpl_longform_vintage_news.json +0 -47
- package/data/templates/tpl_newspic_poster_cover.json +0 -64
- package/data/templates/tpl_square_quote_card.json +0 -46
- package/data/templates/tpl_wechat_cover_editorial.json +0 -64
- package/data/templates/tpl_wechat_cover_minimal.json +0 -64
- package/data/templates/tpl_wide_tech_banner.json +0 -46
- package/dist/assets/index-CZ352D7X.css +0 -1
- package/dist/assets/index-Dpnz-F2g.js +0 -317
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@akairos/typeset-studio",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.3",
|
|
4
4
|
"description": "Algorithmic typesetting studio & zzhub-pipeline image renderer plugin",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"dist/index.html",
|
|
33
33
|
"dist/lib",
|
|
34
34
|
"src",
|
|
35
|
-
"data/templates",
|
|
36
35
|
"public/fonts/README.md",
|
|
37
36
|
"public/fonts/fonts.json",
|
|
38
37
|
"README.md"
|
|
@@ -42,7 +41,7 @@
|
|
|
42
41
|
"dev:server": "bun --watch src/server/index.ts",
|
|
43
42
|
"dev:all": "bun --watch src/server/index.ts & vite",
|
|
44
43
|
"build": "vite build && bun run build:lib",
|
|
45
|
-
"build:lib": "bun build src/index.ts src/adapter.ts --outdir dist/lib --target node --external @napi-rs/canvas",
|
|
44
|
+
"build:lib": "rm -rf dist/lib && bun build src/index.ts src/adapter.ts --outdir dist/lib --target node --splitting --external @napi-rs/canvas",
|
|
46
45
|
"build:watch": "vite build --watch",
|
|
47
46
|
"preview": "vite preview",
|
|
48
47
|
"server": "bun run src/server/index.ts",
|
|
@@ -55,13 +54,10 @@
|
|
|
55
54
|
"dependencies": {
|
|
56
55
|
"@chenglou/pretext": "^0.0.8",
|
|
57
56
|
"@napi-rs/canvas": "^1.0.10",
|
|
58
|
-
"clsx": "^2.1.1",
|
|
59
57
|
"hono": "^4.7.4",
|
|
60
|
-
"html-to-image": "^1.11.11",
|
|
61
58
|
"lucide-react": "^1.16.0",
|
|
62
59
|
"react": "^19.0.0",
|
|
63
|
-
"react-dom": "^19.0.0"
|
|
64
|
-
"tailwind-merge": "^3.0.2"
|
|
60
|
+
"react-dom": "^19.0.0"
|
|
65
61
|
},
|
|
66
62
|
"devDependencies": {
|
|
67
63
|
"@types/bun": "latest",
|
package/src/adapter.test.ts
CHANGED
|
@@ -140,3 +140,120 @@ describe("Typeset Studio — zzhub-pipeline ImageRenderPlugin Adapter", () => {
|
|
|
140
140
|
expect(result.pageCount).toBe(0);
|
|
141
141
|
});
|
|
142
142
|
});
|
|
143
|
+
|
|
144
|
+
describe("Remote adapter regressions", () => {
|
|
145
|
+
it("authenticates, transfers local bytes and preserves page image summaries", async () => {
|
|
146
|
+
const { app } = await import("./server/index");
|
|
147
|
+
const { getOrGenerateAdminKey } = await import("./server/auth");
|
|
148
|
+
const { createCanvas } = await import("@napi-rs/canvas");
|
|
149
|
+
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
150
|
+
const { tmpdir } = await import("node:os");
|
|
151
|
+
const directory = await mkdtemp(join(tmpdir(), "typeset-remote-"));
|
|
152
|
+
const imagePath = join(directory, "source.png");
|
|
153
|
+
await writeFile(imagePath, createCanvas(8, 8).toBuffer("image/png"));
|
|
154
|
+
const requests: any[] = [];
|
|
155
|
+
const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: async (request) => {
|
|
156
|
+
if (request.url.endsWith("/api/render/export")) requests.push(await request.clone().json());
|
|
157
|
+
return app.fetch(request);
|
|
158
|
+
} });
|
|
159
|
+
const previousUrl = process.env.TYPESET_STUDIO_URL;
|
|
160
|
+
const previousKey = process.env.TYPESET_ADMIN_KEY;
|
|
161
|
+
process.env.TYPESET_STUDIO_URL = server.url.toString();
|
|
162
|
+
process.env.TYPESET_ADMIN_KEY = getOrGenerateAdminKey();
|
|
163
|
+
try {
|
|
164
|
+
const progress: string[] = [];
|
|
165
|
+
const output = await typesetStudioAdapter.render({
|
|
166
|
+
route: "wechat-newspic", outputDir: directory, title: "远程测试", bodyText: "完整正文".repeat(100),
|
|
167
|
+
state: { route: { account: "default" } },
|
|
168
|
+
minPages: 3, maxPages: 8,
|
|
169
|
+
bodyImages: [{ marker: "one", path: imagePath }],
|
|
170
|
+
pageSpecs: [{ page: 2, imageMarkers: ["one"], targetFillRatio: 0.5 }],
|
|
171
|
+
accountVisualParams: { bg: "#123456", highlight: "#abcdef", footer: "测试署名", fallbackIcon: imagePath },
|
|
172
|
+
onProgress: (event) => progress.push(event.stage),
|
|
173
|
+
});
|
|
174
|
+
expect(output.pageCount).toBeGreaterThanOrEqual(3);
|
|
175
|
+
expect(output.pages[1].imageCount).toBe(1);
|
|
176
|
+
expect(output.pages[1].imageSources).toEqual([imagePath]);
|
|
177
|
+
expect(requests[1].images[0].src).toStartWith("data:image/png;base64,");
|
|
178
|
+
expect(requests[1].minPages).toBe(3);
|
|
179
|
+
expect(requests[1].pageFillRatios[2]).toBe(0.5);
|
|
180
|
+
expect(requests[1].config.theme.bgColor).toBe("#123456");
|
|
181
|
+
expect(requests[0].config.images[0].id).toBe("account_icon");
|
|
182
|
+
expect(progress).toEqual(["typeset", "cover", "content"]);
|
|
183
|
+
process.env.TYPESET_ADMIN_KEY = "wrong-test-key";
|
|
184
|
+
await expect(typesetStudioAdapter.render({
|
|
185
|
+
route: "wechat-article", outputDir: directory, title: "失败", bodyText: "", state: { route: {} },
|
|
186
|
+
})).rejects.toThrow("401");
|
|
187
|
+
} finally {
|
|
188
|
+
if (previousUrl === undefined) delete process.env.TYPESET_STUDIO_URL; else process.env.TYPESET_STUDIO_URL = previousUrl;
|
|
189
|
+
if (previousKey === undefined) delete process.env.TYPESET_ADMIN_KEY; else process.env.TYPESET_ADMIN_KEY = previousKey;
|
|
190
|
+
server.stop(true);
|
|
191
|
+
await rm(directory, { recursive: true, force: true });
|
|
192
|
+
}
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("uses selected/default Studio templates and produces identical fixed assets locally and remotely", async () => {
|
|
197
|
+
const { app } = await import("./server/index");
|
|
198
|
+
const { getOrGenerateAdminKey } = await import("./server/auth");
|
|
199
|
+
const { getDefaultConfig } = await import("./core/engine");
|
|
200
|
+
const { getSlotDefaultTemplateId, setSlotDefaultTemplateId } = await import("./server/routes/templates");
|
|
201
|
+
const { createCanvas } = await import("@napi-rs/canvas");
|
|
202
|
+
const { mkdtemp, writeFile, rm } = await import("node:fs/promises");
|
|
203
|
+
const { tmpdir } = await import("node:os");
|
|
204
|
+
const directory = await mkdtemp(join(tmpdir(), "typeset-fixed-adapter-"));
|
|
205
|
+
const source = join(directory, "background.png");
|
|
206
|
+
const replacement = join(directory, "replacement.png");
|
|
207
|
+
const canvas = createCanvas(20, 20);
|
|
208
|
+
const context = canvas.getContext("2d");
|
|
209
|
+
context.fillStyle = "#ff0000";
|
|
210
|
+
context.fillRect(0, 0, 20, 20);
|
|
211
|
+
await writeFile(source, canvas.toBuffer("image/png"));
|
|
212
|
+
context.fillStyle = "#00ff00";
|
|
213
|
+
context.fillRect(0, 0, 20, 20);
|
|
214
|
+
await writeFile(replacement, canvas.toBuffer("image/png"));
|
|
215
|
+
const headers = { "Content-Type": "application/json", "x-admin-key": getOrGenerateAdminKey() };
|
|
216
|
+
const templateId = "fixed_adapter_test";
|
|
217
|
+
const config = getDefaultConfig("square-1-1");
|
|
218
|
+
const previousUrl = process.env.TYPESET_STUDIO_URL;
|
|
219
|
+
const previousKey = process.env.TYPESET_ADMIN_KEY;
|
|
220
|
+
const previousDefault = getSlotDefaultTemplateId("newspic_cover", "fixed-test");
|
|
221
|
+
const server = Bun.serve({ hostname: "127.0.0.1", port: 0, fetch: (request) => app.fetch(request) });
|
|
222
|
+
try {
|
|
223
|
+
const saved = await app.request("/api/templates", { method: "POST", headers, body: JSON.stringify({
|
|
224
|
+
...config, id: templateId, images: [{ id: "bg", src: source, alt: "", templateAssetKey: "background", repeatEveryPage: true,
|
|
225
|
+
isAbsolute: true, x: -100, y: -100, width: 1080, height: 1080, zIndex: 0, borderRadius: 0 }],
|
|
226
|
+
}) });
|
|
227
|
+
expect(saved.status).toBe(200);
|
|
228
|
+
setSlotDefaultTemplateId("newspic_cover", templateId, "fixed-test");
|
|
229
|
+
delete process.env.TYPESET_STUDIO_URL;
|
|
230
|
+
const input: ImageRenderInput = { route: "wechat-newspic", title: "模板测试", bodyText: "", outputDir: join(directory, "local"),
|
|
231
|
+
state: { route: { account: "fixed-test" } }, template: "poster-3-4", theme: "unrelated-theme",
|
|
232
|
+
accountVisualParams: { bg: "#123456", fallbackIcon: "/must-not-load.png" },
|
|
233
|
+
};
|
|
234
|
+
const defaultOutput = await typesetStudioAdapter.render(input);
|
|
235
|
+
expect(defaultOutput.pages[0].imageCount).toBe(0);
|
|
236
|
+
const selected = { ...input, studioTemplates: { cover: templateId }, templateImageOverrides: { cover: { background: replacement } } };
|
|
237
|
+
const local = await typesetStudioAdapter.render({ ...selected, outputDir: join(directory, "override") });
|
|
238
|
+
expect(readFileSync(local.assets[0].path).equals(readFileSync(defaultOutput.assets[0].path))).toBe(false);
|
|
239
|
+
process.env.TYPESET_STUDIO_URL = server.url.toString();
|
|
240
|
+
process.env.TYPESET_ADMIN_KEY = getOrGenerateAdminKey();
|
|
241
|
+
const remote = await typesetStudioAdapter.render({ ...selected, outputDir: join(directory, "remote") });
|
|
242
|
+
expect(readFileSync(remote.assets[0].path).equals(readFileSync(local.assets[0].path))).toBe(true);
|
|
243
|
+
expect(remote.pages[0]).toEqual({ page: 1, imageCount: 0, imageSources: [] });
|
|
244
|
+
const resolved = await (await app.request(`/api/templates/${templateId}`, { headers })).json();
|
|
245
|
+
expect(resolved.template.images[0].src).toBe(source);
|
|
246
|
+
const unknown = { ...selected, templateImageOverrides: { cover: { missing: replacement } } };
|
|
247
|
+
await expect(typesetStudioAdapter.render(unknown)).rejects.toThrow("未知模板素材");
|
|
248
|
+
await expect(typesetStudioAdapter.render({ ...selected,
|
|
249
|
+
templateImageOverrides: { cover: { background: join(directory, "missing.png") } },
|
|
250
|
+
})).rejects.toThrow("模板素材 background 加载失败");
|
|
251
|
+
} finally {
|
|
252
|
+
server.stop(true);
|
|
253
|
+
await app.request(`/api/templates/${templateId}`, { method: "DELETE", headers });
|
|
254
|
+
setSlotDefaultTemplateId("newspic_cover", previousDefault, "fixed-test");
|
|
255
|
+
if (previousUrl === undefined) delete process.env.TYPESET_STUDIO_URL; else process.env.TYPESET_STUDIO_URL = previousUrl;
|
|
256
|
+
if (previousKey === undefined) delete process.env.TYPESET_ADMIN_KEY; else process.env.TYPESET_ADMIN_KEY = previousKey;
|
|
257
|
+
await rm(directory, { recursive: true, force: true });
|
|
258
|
+
}
|
|
259
|
+
});
|