@litfamily/litmuse 0.1.1 → 0.1.2
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/CHANGELOG.md +11 -0
- package/README.md +20 -0
- package/README_ko-KR.md +20 -0
- package/bin/litmuse.mjs +4 -0
- package/lib/stage-card.mjs +180 -5
- package/package.json +1 -1
- package/plugin/.muse-plugin/plugin.json +26 -11
- package/plugin/commands/lit-status.md +2 -1
- package/plugin/commands/lit.md +1 -1
- package/plugin/hooks/lib/digest.mjs +1 -1
- package/plugin/hooks/lib/next-step.mjs +68 -0
- package/plugin/hooks/lib/routes.mjs +59 -0
- package/plugin/hooks/lib/workflow-stack.mjs +318 -0
- package/plugin/hooks/session-start.mjs +29 -2
- package/plugin/hooks/user-prompt-submit.mjs +138 -7
- package/plugin/lib/capped-fetch.mjs +25 -0
- package/plugin/lib/pinned-fetch.mjs +79 -0
- package/plugin/lib/plan-task-rows.mjs +32 -0
- package/plugin/lib/render_pages.py +29 -1
- package/plugin/lib/rewind/collect.mjs +679 -0
- package/plugin/lib/rewind/gauge.mjs +72 -0
- package/plugin/lib/rewind/next-step.mjs +91 -0
- package/plugin/lib/rewind/redact.mjs +83 -0
- package/plugin/lib/rewind/transcripts.mjs +227 -0
- package/plugin/skills/frontend-ui-ux/SKILL.md +24 -9
- package/plugin/skills/frontend-ui-ux/kit/components.css +414 -0
- package/plugin/skills/frontend-ui-ux/kit/fonts.json +22 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/landing.html +164 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/settings.html +114 -0
- package/plugin/skills/frontend-ui-ux/kit/layouts/workspace.html +115 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/computational-brutalism.json +227 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/field-notes.json +205 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/graphite.json +200 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/meadow.json +199 -0
- package/plugin/skills/frontend-ui-ux/kit/styles/neo-brutalism.json +222 -0
- package/plugin/skills/frontend-ui-ux/kit/ui.js +226 -0
- package/plugin/skills/frontend-ui-ux/references/complete-contract.md +2 -1
- package/plugin/skills/frontend-ui-ux/references/craft-floor.md +9 -4
- package/plugin/skills/frontend-ui-ux/references/default-editorial-pixel.json +59 -7
- package/plugin/skills/frontend-ui-ux/references/house-styles.md +234 -0
- package/plugin/skills/frontend-ui-ux/references/motion-guide.md +3 -3
- package/plugin/skills/frontend-ui-ux/references/slop-register.md +7 -7
- package/plugin/skills/frontend-ui-ux/scripts/design-lock.mjs +400 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-lock.mjs +196 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-page.mjs +31 -10
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-rules.mjs +19 -7
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe-static.mjs +4 -0
- package/plugin/skills/frontend-ui-ux/scripts/interface-probe.mjs +59 -7
- package/plugin/skills/frontend-ui-ux/scripts/pixel-art.mjs +60 -0
- package/plugin/skills/lit-adhd/SKILL.md +188 -0
- package/plugin/skills/lit-adhd/references/card-examples.md +121 -0
- package/plugin/skills/lit-adhd/references/status-taxonomy.md +43 -0
- package/plugin/skills/lit-adhd/scripts/rewind.mjs +11 -0
- package/plugin/skills/lit-diagram-drawer/SKILL.md +3 -3
- package/plugin/skills/lit-diagram-drawer/scripts/diagram-craft-rules.mjs +1 -2
- package/plugin/skills/lit-diagram-drawer/scripts/visual-quality.mjs +0 -1
- package/plugin/skills/lit-docx/SKILL.md +1 -1
- package/plugin/skills/lit-hijack/SKILL.md +589 -0
- package/plugin/skills/lit-hijack/references/capture-and-provenance.md +123 -0
- package/plugin/skills/lit-hijack/references/clean-room-build.md +133 -0
- package/plugin/skills/lit-hijack/references/distil-and-licence.md +128 -0
- package/plugin/skills/lit-hijack/references/gap-and-port-design.md +152 -0
- package/plugin/skills/lit-hijack/references/parity-and-report.md +133 -0
- package/plugin/skills/lit-hijack/scripts/hijack.mjs +879 -0
- package/plugin/skills/lit-pptx/SKILL.md +20 -9
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Bold.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/JetBrainsMono-Regular.ttf +0 -0
- package/plugin/skills/lit-pptx/fonts/jetbrains-mono/OFL.txt +93 -0
- package/plugin/skills/lit-pptx/fonts/provenance.json +32 -1
- package/plugin/skills/lit-pptx/references/direction-step.md +19 -2
- package/plugin/skills/lit-pptx/references/examples/13-engineering-review-computational-brutalism-ko.md +173 -0
- package/plugin/skills/lit-pptx/references/examples/14-launch-pitch-neo-brutalism-ko.md +154 -0
- package/plugin/skills/lit-pptx/references/examples/15-design-lecture-bauhaus-ko.md +139 -0
- package/plugin/skills/lit-pptx/references/examples/16-community-programme-riso-ko.md +158 -0
- package/plugin/skills/lit-pptx/references/examples/17-public-data-briefing-newsroom-ko.md +179 -0
- package/plugin/skills/lit-pptx/references/tonalities/bauhaus.md +112 -0
- package/plugin/skills/lit-pptx/references/tonalities/computational-brutalism.md +118 -0
- package/plugin/skills/lit-pptx/references/tonalities/neo-brutalism.md +125 -0
- package/plugin/skills/lit-pptx/references/tonalities/newsroom.md +115 -0
- package/plugin/skills/lit-pptx/references/tonalities/riso.md +109 -0
- package/plugin/skills/lit-pptx/scripts/compile-deck.js +1 -1
- package/plugin/skills/lit-pptx/scripts/craft_extras.py +136 -92
- package/plugin/skills/lit-pptx/scripts/embed_fonts.py +13 -1
- package/plugin/skills/lit-pptx/scripts/lib/grid-resolver.js +105 -116
- package/plugin/skills/lit-pptx/scripts/lib/layout-resolver.js +9 -15
- package/plugin/skills/lit-pptx/scripts/lib/render-pack.js +368 -561
- package/plugin/skills/lit-pptx/scripts/lib/slide-grid.js +104 -0
- package/plugin/skills/lit-pptx/scripts/lib/template-registry.js +57 -6
- package/plugin/skills/lit-pptx/scripts/qa_deck.py +4 -4
- package/plugin/skills/lit-pptx/templates/tonalities/atlas/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/bauhaus/pack.yaml +98 -0
- package/plugin/skills/lit-pptx/templates/tonalities/chalk/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/computational-brutalism/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/gazette/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/ledger/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/neo-brutalism/pack.yaml +107 -0
- package/plugin/skills/lit-pptx/templates/tonalities/newsroom/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/night/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/paper/pack.yaml +1 -1
- package/plugin/skills/lit-pptx/templates/tonalities/riso/pack.yaml +99 -0
- package/plugin/skills/lit-pptx/templates/tonalities/signal/pack.yaml +2 -2
- package/plugin/skills/lit-pptx/templates/tonalities/studio/pack.yaml +1 -1
- package/plugin/skills/lit-recap/SKILL.md +3 -0
- package/plugin/skills/lit-schematic/SKILL.md +241 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/build.mjs +14 -0
- package/plugin/skills/lit-schematic/examples/iso-heat-exchanger/scene.mjs +8 -0
- package/plugin/skills/lit-schematic/examples/journal-adsorption-rig/build.mjs +22 -0
- package/plugin/skills/lit-schematic/examples/pid-dosing-skid/build.mjs +73 -0
- package/plugin/skills/lit-schematic/references/cutaway-outputs.md +25 -0
- package/plugin/skills/lit-schematic/references/isometric-craft.md +24 -0
- package/plugin/skills/lit-schematic/references/journal-figures.md +19 -0
- package/plugin/skills/lit-schematic/references/kit-api.md +27 -0
- package/plugin/skills/lit-schematic/references/pfd-and-layout.md +20 -0
- package/plugin/skills/lit-schematic/references/pid-drafting.md +26 -0
- package/plugin/skills/lit-schematic/references/pid-symbols.md +27 -0
- package/plugin/skills/lit-schematic/references/verification.md +24 -0
- package/plugin/skills/lit-schematic/scripts/doctor.mjs +13 -0
- package/plugin/skills/lit-schematic/scripts/lib/figure.mjs +49 -0
- package/plugin/skills/lit-schematic/scripts/lib/isa.mjs +18 -0
- package/plugin/skills/lit-schematic/scripts/lib/iso.mjs +58 -0
- package/plugin/skills/lit-schematic/scripts/lib/legend-sheet.mjs +10 -0
- package/plugin/skills/lit-schematic/scripts/lib/qa-core.mjs +241 -0
- package/plugin/skills/lit-schematic/scripts/lib/sheet.mjs +446 -0
- package/plugin/skills/lit-schematic/scripts/lib/svg.mjs +39 -0
- package/plugin/skills/lit-schematic/scripts/lib/theme.mjs +25 -0
- package/plugin/skills/lit-schematic/scripts/motion.mjs +8 -0
- package/plugin/skills/lit-schematic/scripts/qa.mjs +21 -0
- package/plugin/skills/lit-schematic/scripts/render.mjs +38 -0
- package/plugin/skills/lit-schematic/scripts/scaffold.mjs +73 -0
- package/plugin/skills/lit-scientific-visualization/SKILL.md +2 -0
- package/plugin/skills/lit-typographic-motion/SKILL.md +12 -8
- package/plugin/skills/lit-typographic-motion/engine/THIRD_PARTY_NOTICES +11 -0
- package/plugin/skills/lit-typographic-motion/engine/blender/runner.py +218 -0
- package/plugin/skills/lit-typographic-motion/engine/core/presets.mjs +1 -1
- package/plugin/skills/lit-typographic-motion/engine/node/assets.mjs +218 -0
- package/plugin/skills/lit-typographic-motion/engine/node/blender.mjs +144 -0
- package/plugin/skills/lit-typographic-motion/engine/node/board.mjs +136 -0
- package/plugin/skills/lit-typographic-motion/engine/node/done.mjs +42 -3
- package/plugin/skills/lit-typographic-motion/engine/node/gallery.mjs +120 -0
- package/plugin/skills/lit-typographic-motion/engine/node/house.mjs +44 -0
- package/plugin/skills/lit-typographic-motion/engine/node/pipeline.mjs +18 -7
- package/plugin/skills/lit-typographic-motion/engine/node/runtime.mjs +6 -3
- package/plugin/skills/lit-typographic-motion/engine/node/scaffold.mjs +210 -0
- package/plugin/skills/lit-typographic-motion/engine/node/score.mjs +639 -0
- package/plugin/skills/lit-typographic-motion/engine/node/sound-cli.mjs +21 -4
- package/plugin/skills/lit-typographic-motion/engine/node/sound.mjs +126 -27
- package/plugin/skills/lit-typographic-motion/engine/node/stage-serve.mjs +29 -5
- package/plugin/skills/lit-typographic-motion/engine/node/stage.mjs +82 -14
- package/plugin/skills/lit-typographic-motion/engine/node/styles.mjs +214 -0
- package/plugin/skills/lit-typographic-motion/engine/node/treatment.mjs +311 -10
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/LICENSE +21 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/environments/RoomEnvironment.js +144 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/addons/geometries/RoundedBoxGeometry.js +155 -0
- package/plugin/skills/lit-typographic-motion/engine/vendor/three/three.module.js +54571 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/expected.json +69 -0
- package/plugin/skills/lit-typographic-motion/recipes/hero-3d/index.html +139 -0
- package/plugin/skills/lit-typographic-motion/references/depth.md +122 -0
- package/plugin/skills/lit-typographic-motion/references/sound.md +74 -16
- package/plugin/skills/lit-typographic-motion/references/stage.md +29 -5
- package/plugin/skills/lit-typographic-motion/references/studio.md +245 -0
- package/plugin/skills/lit-typographic-motion/references/treatment.md +31 -8
- package/plugin/skills/lit-typographic-motion/scripts/motion.mjs +48 -7
- package/plugin/skills/lit-typographic-motion/styles/paper.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/swiss-signal.json +41 -0
- package/plugin/skills/lit-typographic-motion/styles/terminalcore.json +42 -0
- package/plugin/skills/lit-typographic-motion/styles/tidal.json +41 -0
- package/plugin/skills/readme-studio/SKILL.md +1 -1
- package/plugin/skills/start-work/SKILL.md +6 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Bounded response bodies: a fetch that buffers a body reads the stream in pieces
|
|
2
|
+
// and aborts past the limit, so a chunked response without (or lying about)
|
|
3
|
+
// content-length cannot fill the heap before the size check runs.
|
|
4
|
+
export const readCapped = async (response, limit, url) => {
|
|
5
|
+
const chunks = [];
|
|
6
|
+
let size = 0;
|
|
7
|
+
if (response.body && typeof response.body[Symbol.asyncIterator] === 'function') {
|
|
8
|
+
for await (const chunk of response.body) {
|
|
9
|
+
size += chunk.length;
|
|
10
|
+
if (size > limit) {
|
|
11
|
+
try {
|
|
12
|
+
await response.body.cancel();
|
|
13
|
+
} catch {
|
|
14
|
+
// Already closed by the reader.
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`${url} is larger than ${limit} bytes`);
|
|
17
|
+
}
|
|
18
|
+
chunks.push(chunk);
|
|
19
|
+
}
|
|
20
|
+
return Buffer.concat(chunks);
|
|
21
|
+
}
|
|
22
|
+
const whole = Buffer.from(await response.arrayBuffer());
|
|
23
|
+
if (whole.length > limit) throw new Error(`${url} is larger than ${limit} bytes`);
|
|
24
|
+
return whole;
|
|
25
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
// Pinned HTTPS: resolve once for the hop check, then connect only to the approved
|
|
2
|
+
// address, so a name that answers again with a private address (DNS rebinding) cannot
|
|
3
|
+
// move the connection. TLS still verifies the certificate for the name.
|
|
4
|
+
import { isIP, isIPv4, isIPv6 } from 'node:net';
|
|
5
|
+
import { request as httpsRequest } from 'node:https';
|
|
6
|
+
import { Readable } from 'node:stream';
|
|
7
|
+
|
|
8
|
+
const privateV4 = (ip) => {
|
|
9
|
+
const [a, b] = ip.split('.').map(Number);
|
|
10
|
+
return a === 0 || a === 10 || a === 127 || a >= 224 || (a === 100 && b >= 64 && b <= 127) || (a === 169 && b === 254)
|
|
11
|
+
|| (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 198 && (b === 18 || b === 19));
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// Loopback, link-local, private, CGNAT, benchmark, multicast and cloud-metadata ranges,
|
|
15
|
+
// plus IPv4-mapped IPv6 forms of the same. Anything else, including bare names, is public.
|
|
16
|
+
export const isPrivateAddress = (address) => {
|
|
17
|
+
const ip = String(address).replace(/^\[|\]$/gu, '').toLowerCase();
|
|
18
|
+
if (isIPv4(ip)) return privateV4(ip);
|
|
19
|
+
if (!isIPv6(ip)) return false;
|
|
20
|
+
const mapped = /^(?:0*:)*:?ffff:(?:(\d+\.\d+\.\d+\.\d+)|([0-9a-f]{1,4}):([0-9a-f]{1,4}))$/u.exec(ip);
|
|
21
|
+
if (mapped) {
|
|
22
|
+
if (mapped[1]) return privateV4(mapped[1]);
|
|
23
|
+
const [high, low] = [mapped[2], mapped[3]].map((part) => Number.parseInt(part, 16));
|
|
24
|
+
return privateV4(`${high >> 8}.${high & 255}.${low >> 8}.${low & 255}`);
|
|
25
|
+
}
|
|
26
|
+
return ip === '::' || ip === '::1' || /^f[cd]/u.test(ip) || /^fe[89ab]/u.test(ip) || /^ff/u.test(ip);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
// One redirect hop: https only, no local names or addresses, and names must resolve to
|
|
30
|
+
// at least one address with none private. Returns the href with the approved address, so
|
|
31
|
+
// the connect below cannot be moved by a second, private answer.
|
|
32
|
+
export const checkHop = async (href, lookup, httpsNote = 'fetch https only') => {
|
|
33
|
+
let url;
|
|
34
|
+
try {
|
|
35
|
+
url = new URL(href);
|
|
36
|
+
} catch {
|
|
37
|
+
throw new Error(`refusing ${href}: not a URL`);
|
|
38
|
+
}
|
|
39
|
+
if (url.protocol !== 'https:') throw new Error(`refusing ${url.protocol} ${href}: ${httpsNote}`);
|
|
40
|
+
const host = url.hostname.replace(/^\[|\]$/gu, '');
|
|
41
|
+
if (host === 'localhost' || host.endsWith('.localhost') || isPrivateAddress(host)) {
|
|
42
|
+
throw new Error(`refusing ${href}: a loopback, link-local or private host`);
|
|
43
|
+
}
|
|
44
|
+
if (isIP(host)) return { href: url.href, address: host, family: isIP(host) };
|
|
45
|
+
if (!lookup) return { href: url.href, address: null, family: 0 };
|
|
46
|
+
const addresses = await lookup(host, { all: true });
|
|
47
|
+
if (!Array.isArray(addresses) || addresses.length === 0) throw new Error(`refusing ${href}: ${host} does not resolve`);
|
|
48
|
+
if (addresses.some(({ address }) => isPrivateAddress(address))) {
|
|
49
|
+
throw new Error(`refusing ${href}: ${host} resolves to a private address`);
|
|
50
|
+
}
|
|
51
|
+
return { href: url.href, address: addresses[0].address, family: addresses[0].family };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// node:https with a lookup that answers only with the approved address, read back as a
|
|
55
|
+
// Response. `request` is injectable for tests; pass a real name and the TLS certificate
|
|
56
|
+
// is still verified for it.
|
|
57
|
+
export const pinnedSend = (request = httpsRequest) => (href, { headers, signal }, { address, family }) => new Promise((resolveResponse, reject) => {
|
|
58
|
+
const pin = (host, options, callback) => {
|
|
59
|
+
if (typeof options === 'function') { options(null, address, family); return; }
|
|
60
|
+
if (options && options.all) callback(null, [{ address, family }]);
|
|
61
|
+
else callback(null, address, family);
|
|
62
|
+
};
|
|
63
|
+
const req = request(href, { headers, signal, lookup: pin }, (res) => {
|
|
64
|
+
const answer = new Headers();
|
|
65
|
+
for (const [name, value] of Object.entries(res.headers ?? {})) {
|
|
66
|
+
for (const item of [value].flat()) if (item !== undefined) answer.append(name, String(item));
|
|
67
|
+
}
|
|
68
|
+
const empty = [204, 205, 304].includes(res.statusCode);
|
|
69
|
+
if (empty && typeof res.resume === 'function') res.resume();
|
|
70
|
+
try {
|
|
71
|
+
resolveResponse(new Response(empty ? null : Readable.toWeb(res), { status: res.statusCode, headers: answer }));
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (typeof res.destroy === 'function') res.destroy();
|
|
74
|
+
reject(error);
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
req.on('error', reject);
|
|
78
|
+
req.end();
|
|
79
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Return source lines that are outside Markdown code fences, preserving line indexes. */
|
|
2
|
+
export const planLinesOutsideFences = (text) => {
|
|
3
|
+
const visible = [];
|
|
4
|
+
let fence = null;
|
|
5
|
+
const lines = text.split(/\r?\n/u);
|
|
6
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
7
|
+
const line = lines[index];
|
|
8
|
+
const fenceMatch = /^ {0,3}(`{3,}|~{3,})(.*)$/u.exec(line);
|
|
9
|
+
if (!fence && fenceMatch) {
|
|
10
|
+
fence = { marker: fenceMatch[1][0], length: fenceMatch[1].length };
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
if (
|
|
14
|
+
fence
|
|
15
|
+
&& fenceMatch
|
|
16
|
+
&& fenceMatch[1][0] === fence.marker
|
|
17
|
+
&& fenceMatch[1].length >= fence.length
|
|
18
|
+
&& fenceMatch[2].trim() === ""
|
|
19
|
+
) {
|
|
20
|
+
fence = null;
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
if (!fence) visible.push({ line, index });
|
|
24
|
+
}
|
|
25
|
+
return visible;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/** Parse the exact column-zero checkbox grammar consumed by start-work. */
|
|
29
|
+
export const parsePlanTaskRows = (text) => planLinesOutsideFences(text).flatMap(({ line, index }) => {
|
|
30
|
+
const match = /^- \[([ xX])\]\s+(.+?)\s*$/u.exec(line);
|
|
31
|
+
return match ? [{ checked: match[1].toLowerCase() === "x", text: match[2], line, index }] : [];
|
|
32
|
+
});
|
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
"""Render a .pptx, .docx or .pdf to PNG pages so a person (or the model) can look at it.
|
|
3
3
|
|
|
4
4
|
LibreOffice (soffice) converts Office files to PDF in a private profile directory,
|
|
5
|
-
so parallel runs never share a lock; PyMuPDF rasterises the PDF pages.
|
|
5
|
+
so parallel runs never share a lock; PyMuPDF rasterises the PDF pages. Fonts a .pptx
|
|
6
|
+
embeds as plain font files are placed in that profile's font folder first, so the pages
|
|
7
|
+
show the faces PowerPoint shows even where the font is not installed. A PDF input
|
|
6
8
|
skips the conversion. Without soffice an Office file cannot be rendered: the script
|
|
7
9
|
prints SKIP with the reason and exits 0, and the caller must say that the visual
|
|
8
10
|
pass did not happen.
|
|
@@ -21,9 +23,34 @@ import shutil
|
|
|
21
23
|
import subprocess
|
|
22
24
|
import sys
|
|
23
25
|
import tempfile
|
|
26
|
+
import zipfile
|
|
24
27
|
from pathlib import Path
|
|
25
28
|
|
|
26
29
|
|
|
30
|
+
def profile_fonts(source: Path, profile: Path) -> list[Path]:
|
|
31
|
+
"""Copy the plain font files a .pptx embeds (ppt/fonts/*.fntdata) into the profile's font folder."""
|
|
32
|
+
if source.suffix.lower() != ".pptx":
|
|
33
|
+
return []
|
|
34
|
+
placed = []
|
|
35
|
+
try:
|
|
36
|
+
with zipfile.ZipFile(source) as z:
|
|
37
|
+
for name in z.namelist():
|
|
38
|
+
if not (name.startswith("ppt/fonts/") and name.endswith(".fntdata")):
|
|
39
|
+
continue
|
|
40
|
+
data = z.read(name)
|
|
41
|
+
# Only a TrueType or OpenType file; an obfuscated or EOT part is left to the renderer.
|
|
42
|
+
kind = ".otf" if data[:4] == b"OTTO" else ".ttf" if data[:4] in (b"\x00\x01\x00\x00", b"true") else None
|
|
43
|
+
if not kind:
|
|
44
|
+
continue
|
|
45
|
+
target = profile / "user" / "fonts" / (Path(name).stem + kind)
|
|
46
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
+
target.write_bytes(data)
|
|
48
|
+
placed.append(target)
|
|
49
|
+
except zipfile.BadZipFile:
|
|
50
|
+
return []
|
|
51
|
+
return placed
|
|
52
|
+
|
|
53
|
+
|
|
27
54
|
def to_pdf(source: Path, out_dir: Path) -> tuple[Path | None, str]:
|
|
28
55
|
if source.suffix.lower() == ".pdf":
|
|
29
56
|
return source, "input is already PDF"
|
|
@@ -31,6 +58,7 @@ def to_pdf(source: Path, out_dir: Path) -> tuple[Path | None, str]:
|
|
|
31
58
|
if not soffice:
|
|
32
59
|
return None, "LibreOffice (soffice) is not on PATH"
|
|
33
60
|
with tempfile.TemporaryDirectory(prefix="litmuse-soffice-") as profile:
|
|
61
|
+
profile_fonts(source, Path(profile))
|
|
34
62
|
result = subprocess.run(
|
|
35
63
|
[
|
|
36
64
|
soffice,
|