优化后端
This commit is contained in:
+178
-9
@@ -92,6 +92,24 @@ const REQUEST_TIMEOUT_MS = 45 * 1000;
|
||||
const DEFAULT_PHYSICAL_STATE_POLL_INTERVAL_MS = 5000;
|
||||
const AUTHORITATIVE_CH1_SOURCES = new Set(["physical_can_ch1", "wireless_mqtt_ch1"]);
|
||||
|
||||
async function apiResponseError(response) {
|
||||
let payload = null;
|
||||
try {
|
||||
payload = await response.json();
|
||||
} catch (_error) {
|
||||
// Some proxies return an empty or non-JSON error page. Preserve the HTTP
|
||||
// status even when no structured reason is available.
|
||||
}
|
||||
const status = Number(response?.status) || 0;
|
||||
const serverMessage = String(payload?.message || payload?.error || "").trim();
|
||||
const fallback = status ? `HTTP ${status}` : "请求失败";
|
||||
const error = new Error(serverMessage ? `${fallback}:${serverMessage}` : fallback);
|
||||
error.status = status;
|
||||
error.code = String(payload?.error || "").trim();
|
||||
error.serverMessage = serverMessage;
|
||||
return error;
|
||||
}
|
||||
|
||||
function requestTimeoutForExecutionMode(mode) {
|
||||
return mode === "hardware" ? HARDWARE_REQUEST_TIMEOUT_MS : REQUEST_TIMEOUT_MS;
|
||||
}
|
||||
@@ -111,6 +129,8 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
if (!indicator) return;
|
||||
const label = $("b", indicator);
|
||||
if (!label) return;
|
||||
indicator.removeAttribute("aria-label");
|
||||
indicator.removeAttribute("title");
|
||||
const summarySystem = $("#cabinSummarySystem");
|
||||
const setSummarySystem = (text, status) => {
|
||||
if (!summarySystem) return;
|
||||
@@ -144,6 +164,22 @@ function renderCabinLiveStatus(networkStatus = state.connectionStatus) {
|
||||
}
|
||||
|
||||
const snapshot = state.vehicleStateSnapshot;
|
||||
if (snapshot?.errorStatus === 403) {
|
||||
const reason = snapshot.errorReason || "当前终端没有车辆状态读取权限";
|
||||
const visibleReason = `${transportLabel} · 403:${reason}`;
|
||||
indicator.dataset.state = "offline";
|
||||
label.textContent = visibleReason;
|
||||
indicator.title = visibleReason;
|
||||
indicator.setAttribute("aria-label", visibleReason);
|
||||
const telemetryState = $("#vehicleTelemetryState");
|
||||
if (telemetryState) telemetryState.textContent = "403 拒绝";
|
||||
setSummarySystem("403 · 状态读取被拒绝", "warning");
|
||||
return;
|
||||
}
|
||||
const telemetryState = $("#vehicleTelemetryState");
|
||||
if (telemetryState?.textContent === "403 拒绝") {
|
||||
telemetryState.textContent = "只读";
|
||||
}
|
||||
const observedIds = Array.isArray(snapshot?.observedFrameIds)
|
||||
? snapshot.observedFrameIds
|
||||
: [];
|
||||
@@ -5884,19 +5920,24 @@ function installModeChipScroller() {
|
||||
let pointerStartScroll = 0;
|
||||
let dragged = false;
|
||||
let suppressClick = false;
|
||||
let pointerCaptured = false;
|
||||
group.addEventListener("pointerdown", (event) => {
|
||||
if (event.pointerType !== "mouse" || event.button !== 0 || maximumScroll() <= 1) return;
|
||||
pointerId = event.pointerId;
|
||||
pointerStartX = event.clientX;
|
||||
pointerStartScroll = group.scrollLeft;
|
||||
dragged = false;
|
||||
group.setPointerCapture?.(pointerId);
|
||||
pointerCaptured = false;
|
||||
});
|
||||
group.addEventListener("pointermove", (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
const distance = event.clientX - pointerStartX;
|
||||
if (!dragged && Math.abs(distance) < 5) return;
|
||||
dragged = true;
|
||||
if (!pointerCaptured && typeof group.setPointerCapture === "function") {
|
||||
group.setPointerCapture(pointerId);
|
||||
pointerCaptured = true;
|
||||
}
|
||||
group.classList.add("is-dragging");
|
||||
group.scrollLeft = Math.max(0, Math.min(maximumScroll(), pointerStartScroll - distance));
|
||||
updateScrollState();
|
||||
@@ -5904,8 +5945,9 @@ function installModeChipScroller() {
|
||||
});
|
||||
const finishPointerDrag = (event) => {
|
||||
if (pointerId !== event.pointerId) return;
|
||||
group.releasePointerCapture?.(pointerId);
|
||||
if (pointerCaptured) group.releasePointerCapture?.(pointerId);
|
||||
pointerId = null;
|
||||
pointerCaptured = false;
|
||||
group.classList.remove("is-dragging");
|
||||
if (dragged) {
|
||||
suppressClick = true;
|
||||
@@ -6094,11 +6136,113 @@ function installModeActionPreview() {
|
||||
window.addEventListener("resize", () => hideModeActionPopover({ immediate: true }), { passive: true });
|
||||
}
|
||||
|
||||
function vueModeChipEntry(mode) {
|
||||
const timeline = mode?.timeline;
|
||||
const actionCount = modeExecutionActionViews(mode).length
|
||||
|| Number(mode?.operation_count || 0);
|
||||
const phaseSummary = Array.isArray(timeline?.phases)
|
||||
? timeline.phases.map((phase) => {
|
||||
if (Number(phase.duration_ms || 0) > 0) {
|
||||
return `${phase.name} ${modeSecondsLabel(phase.duration_ms)}s`;
|
||||
}
|
||||
const timeout = Number(phase.timeout_ms || 0);
|
||||
return timeout > 0
|
||||
? `${phase.name} 目标到位(保护上限${modeSecondsLabel(timeout)}s)`
|
||||
: `${phase.name} 目标到位`;
|
||||
}).join(" → ")
|
||||
: "";
|
||||
let cycleLabel = "";
|
||||
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
|
||||
const runtimeMinutes = Number(timeline.max_runtime_ms || 0) / 60000;
|
||||
const runtimeLabel = Number.isInteger(runtimeMinutes)
|
||||
? String(runtimeMinutes)
|
||||
: runtimeMinutes.toFixed(1);
|
||||
const cycleDurationMs = Number(timeline.cycle_duration_ms || 0);
|
||||
const cycle = cycleDurationMs > 0
|
||||
? `${modeSecondsLabel(cycleDurationMs)}s循环`
|
||||
: "目标到位循环";
|
||||
cycleLabel = runtimeMinutes > 0 ? `${cycle} · 最长${runtimeLabel}min` : cycle;
|
||||
}
|
||||
return {
|
||||
type: "mode",
|
||||
key: `mode-${mode.id}`,
|
||||
id: mode.id,
|
||||
label: modeChipLabel(mode),
|
||||
summary: modeChipSummary(mode),
|
||||
icon: mode.icon || "bookmark",
|
||||
command: `进入${mode.name}`,
|
||||
origin: mode?.origin || "preset",
|
||||
behavior: cycleLabel ? "cyclic" : "",
|
||||
cycleLabel,
|
||||
ariaLabel: `${mode.name || mode.id},${actionCount} 项执行动作,点击执行`,
|
||||
title: [mode.description || mode.name, phaseSummary, "悬浮或聚焦查看全部执行动作"]
|
||||
.filter(Boolean)
|
||||
.join("\n"),
|
||||
};
|
||||
}
|
||||
|
||||
function vueModeChipEntries(modes) {
|
||||
const preferredStaticOrder = [
|
||||
"zero_gravity",
|
||||
"office",
|
||||
"nap",
|
||||
"queen_passenger_driving",
|
||||
"leisure_lounger",
|
||||
];
|
||||
const userModes = modes.filter(isUserModeProfile);
|
||||
const dynamicOrder = new Map([["rocking_horse", 0], ["dynamic_cradle", 1]]);
|
||||
const dynamicModes = modes
|
||||
.filter((mode) => !isUserModeProfile(mode) && isDynamicModeProfile(mode))
|
||||
.sort((left, right) => (
|
||||
(dynamicOrder.get(left.id) ?? Number.MAX_SAFE_INTEGER)
|
||||
- (dynamicOrder.get(right.id) ?? Number.MAX_SAFE_INTEGER)
|
||||
));
|
||||
const staticModes = modes.filter(
|
||||
(mode) => !isUserModeProfile(mode) && !isDynamicModeProfile(mode),
|
||||
);
|
||||
const preferredStaticModes = preferredStaticOrder
|
||||
.map((modeId) => staticModes.find((mode) => mode.id === modeId))
|
||||
.filter(Boolean);
|
||||
const otherStaticModes = staticModes.filter((mode) => !preferredStaticModes.includes(mode));
|
||||
const entries = [];
|
||||
const appendLabel = (label, id) => entries.push({
|
||||
type: "label",
|
||||
key: `label-${id}`,
|
||||
id,
|
||||
label,
|
||||
});
|
||||
const appendDivider = (id) => entries.push({
|
||||
type: "divider",
|
||||
key: `divider-${id}`,
|
||||
id,
|
||||
});
|
||||
if (userModes.length) {
|
||||
appendLabel("自定义模式", "custom");
|
||||
entries.push(...userModes.map(vueModeChipEntry));
|
||||
}
|
||||
if (userModes.length && (staticModes.length || dynamicModes.length)) appendDivider("custom");
|
||||
if (staticModes.length) {
|
||||
appendLabel("静态模式", "static");
|
||||
entries.push(...preferredStaticModes.map(vueModeChipEntry));
|
||||
entries.push(...otherStaticModes.map(vueModeChipEntry));
|
||||
}
|
||||
if (dynamicModes.length) {
|
||||
if (userModes.length || staticModes.length) appendDivider("dynamic");
|
||||
appendLabel("动态模式", "dynamic");
|
||||
entries.push(...dynamicModes.map(vueModeChipEntry));
|
||||
}
|
||||
return entries;
|
||||
}
|
||||
|
||||
function populateModeChips() {
|
||||
const group = $(".mode-chips");
|
||||
if (!group) return;
|
||||
const buttons = () => $$("button", group);
|
||||
const modes = modeProfiles();
|
||||
if (window.CognitiveAgentVueUI?.renderModeChips?.(group, vueModeChipEntries(modes))) {
|
||||
populateModeGallery();
|
||||
return;
|
||||
}
|
||||
const runtimeModeIds = new Set(modes.map((mode) => mode.id));
|
||||
$$('button[data-mode-origin="user"]', group).forEach((button) => {
|
||||
if (!runtimeModeIds.has(button.dataset.modeProfileId)) button.remove();
|
||||
@@ -6155,6 +6299,8 @@ function populateModeChips() {
|
||||
runIcon.dataset.lucide = "play";
|
||||
button.append(runIcon);
|
||||
}
|
||||
runIcon.dataset.modeRun = "true";
|
||||
runIcon.title = `执行${mode.name || mode.id}`;
|
||||
const timeline = mode.timeline;
|
||||
let cycleBadge = $(".mode-cycle-badge", button);
|
||||
if (["infinite", "until_stopped_or_timeout"].includes(timeline?.repeat)) {
|
||||
@@ -6330,8 +6476,10 @@ function setModeChipsDisabled(disabled) {
|
||||
});
|
||||
}
|
||||
|
||||
async function executeSavedModeProfile(mode) {
|
||||
async function executeSavedModeProfile(mode, { command = "" } = {}) {
|
||||
if (!mode?.id || !mode?.command_flow?.source_request_id) return false;
|
||||
const modeCommand = String(command || `进入${mode.name || mode.id}`).trim();
|
||||
const assistant = window.CognitiveAgentAssistant;
|
||||
const driverOnlyCh1 = isDriverOnlyCh1Runtime();
|
||||
if (state.executionMode === "hardware"
|
||||
&& state.controlTransport !== "wireless_mqtt"
|
||||
@@ -6346,10 +6494,13 @@ async function executeSavedModeProfile(mode) {
|
||||
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
const runButton = $("#runButton");
|
||||
const input = $("#utterance");
|
||||
runButton.disabled = true;
|
||||
setModeChipsDisabled(true);
|
||||
setModeExecutionState(mode.id, "running");
|
||||
$("#utterance").value = mode.name || mode.id;
|
||||
assistant?.open?.();
|
||||
input.value = modeCommand;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
setRunStatus("processing", "MODE COMMAND FLOW");
|
||||
$("#resultTitle").textContent = `正在直接执行${mode.name}`;
|
||||
try {
|
||||
@@ -6375,11 +6526,11 @@ async function executeSavedModeProfile(mode) {
|
||||
if (!payload) throw new Error("模式卡片未返回执行结果");
|
||||
renderResult(payload);
|
||||
const completed = ["succeeded", "partially_succeeded"].includes(payload.status);
|
||||
$("#utterance").value = mode.name || mode.id;
|
||||
$("#resultTitle").textContent = completed
|
||||
? `${mode.name}执行完成`
|
||||
: `${mode.name}执行未完成`;
|
||||
setModeExecutionState(mode.id, completed ? "succeeded" : "failed");
|
||||
assistant?.announceModeExecution?.(mode, payload, { command: modeCommand });
|
||||
if (state.executionMode === "hardware") await synchronizePhysicalSeatState();
|
||||
showToast(completed
|
||||
? `${mode.name}已直接执行保存的控制指令流`
|
||||
@@ -6388,8 +6539,13 @@ async function executeSavedModeProfile(mode) {
|
||||
setModeExecutionState(mode.id, "failed");
|
||||
setRunStatus("failed", "MODE FAILED");
|
||||
$("#resultTitle").textContent = `${mode.name}执行失败`;
|
||||
assistant?.announceModeExecution?.(mode, null, { command: modeCommand, error });
|
||||
showToast(error.message || "模式执行失败");
|
||||
} finally {
|
||||
if (input.value === modeCommand) {
|
||||
input.value = "";
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
}
|
||||
runButton.disabled = false;
|
||||
setModeChipsDisabled(false);
|
||||
}
|
||||
@@ -6412,6 +6568,10 @@ function applyModeCommand(command, modeProfileId = null) {
|
||||
setModeRailSelection(mode);
|
||||
closeModeGallery({ restoreFocus: false });
|
||||
assistant?.open?.();
|
||||
if (mode?.command_flow?.source_request_id) {
|
||||
void executeSavedModeProfile(mode, { command: modeCommand });
|
||||
return true;
|
||||
}
|
||||
const input = $("#utterance");
|
||||
const form = $("#requestForm");
|
||||
if (!input || !form) {
|
||||
@@ -6468,7 +6628,7 @@ function applyRuntimeTargetPolicy(runtime) {
|
||||
async function loadRuntime() {
|
||||
try {
|
||||
const response = await apiRequest("/api/runtime", { cache: "no-store" });
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
state.runtime = await response.json();
|
||||
renderControlTransport();
|
||||
applyRuntimeTargetPolicy(state.runtime);
|
||||
@@ -6487,6 +6647,7 @@ async function loadRuntime() {
|
||||
mqttDualChannel ? vehicleCan.configured : hardware.configured,
|
||||
);
|
||||
const vehicleHardwareReady = Boolean(vehicleCan.configured);
|
||||
const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;
|
||||
const ch1DriverOnly = vehicleHardwareReady && !seatHardwareReady;
|
||||
const hardwareExecutionReady = Boolean(hardware.configured || vehicleCan.configured);
|
||||
const gptButton = $('[data-provider="openai"]');
|
||||
@@ -6563,7 +6724,7 @@ async function synchronizePassengerSeatState() {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("passenger", true, requestedTransport);
|
||||
@@ -6605,7 +6766,7 @@ async function synchronizeDriverSeatState() {
|
||||
window.location.replace("/login");
|
||||
return false;
|
||||
}
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
if (!response.ok) throw await apiResponseError(response);
|
||||
const payload = await response.json();
|
||||
if (requestedTransport !== state.controlTransport) return false;
|
||||
updateWiredCanProbe("vehicle", true, requestedTransport);
|
||||
@@ -6627,6 +6788,9 @@ async function synchronizeDriverSeatState() {
|
||||
state.vehicleStateSnapshot = {
|
||||
...(state.vehicleStateSnapshot || {}),
|
||||
error: error.message || String(error),
|
||||
errorStatus: Number(error.status) || null,
|
||||
errorCode: error.code || "",
|
||||
errorReason: error.serverMessage || error.message || String(error),
|
||||
};
|
||||
renderCabinLiveStatus();
|
||||
if (!["physical_can_ch1", "wireless_mqtt_ch1"].includes(state.seatFeedbackSources.driver)) {
|
||||
@@ -7950,6 +8114,10 @@ function recordVehicleStateTelemetry(payload) {
|
||||
: "physical_can_ch1";
|
||||
state.vehicleStateSnapshot = {
|
||||
...previous,
|
||||
error: null,
|
||||
errorStatus: null,
|
||||
errorCode: "",
|
||||
errorReason: "",
|
||||
source,
|
||||
capturedAt: payload.captured_at || null,
|
||||
receivedAt: Date.now(),
|
||||
@@ -8716,7 +8884,8 @@ document.addEventListener("DOMContentLoaded", async () => {
|
||||
installModeActionPreview();
|
||||
$(".mode-chips").addEventListener("click", (event) => {
|
||||
const target = event.target instanceof Element ? event.target : null;
|
||||
const button = target?.closest("button[data-mode-command]");
|
||||
const runControl = target?.closest("[data-mode-run]");
|
||||
const button = (runControl || target)?.closest("button[data-mode-command]");
|
||||
if (button) applyModeCommand(
|
||||
button.dataset.modeCommand,
|
||||
button.dataset.modeProfileId || null,
|
||||
|
||||
@@ -1506,3 +1506,220 @@
|
||||
min-height: 32px !important;
|
||||
}
|
||||
}
|
||||
|
||||
/* Compact command receipt inside each AI assistant response. */
|
||||
.assistant-message-with-dispatched-controls {
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
align-self: stretch;
|
||||
}
|
||||
|
||||
.assistant-message-with-dispatched-controls .assistant-message-body {
|
||||
min-width: 0;
|
||||
flex: 1 1 0;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-vue-host {
|
||||
width: 100%;
|
||||
max-width: 336px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls {
|
||||
width: 100%;
|
||||
max-width: 336px;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
color: var(--assistant-text, #233044);
|
||||
background: color-mix(in oklch, var(--card) 91%, var(--accent));
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: 14px;
|
||||
box-shadow: 0 8px 24px color-mix(in oklch, var(--foreground) 7%, transparent);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary {
|
||||
min-height: 46px;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto 18px;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 11px;
|
||||
color: var(--heading-foreground, #1e2f46);
|
||||
cursor: pointer;
|
||||
list-style: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary::-webkit-details-marker {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary:focus-visible {
|
||||
outline: 2px solid color-mix(in oklch, var(--primary) 42%, transparent);
|
||||
outline-offset: -2px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-heading-icon,
|
||||
.assistant-dispatched-control-icon {
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
color: var(--primary, #2771e7);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-heading-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
background: color-mix(in oklch, var(--primary) 9%, var(--card));
|
||||
border-radius: 9px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-heading-icon svg {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
stroke-width: 1.9;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls > summary > b {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-count {
|
||||
min-width: 25px;
|
||||
height: 22px;
|
||||
display: inline-grid;
|
||||
place-items: center;
|
||||
padding: 0 7px;
|
||||
color: var(--primary, #2771e7);
|
||||
background: color-mix(in oklch, var(--primary) 10%, var(--card));
|
||||
border-radius: 999px;
|
||||
font-size: 10px;
|
||||
font-weight: 850;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-chevron {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
color: var(--muted-foreground, #67768b);
|
||||
transition: transform 160ms ease;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls[open] > summary .assistant-dispatched-controls-chevron {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li {
|
||||
min-height: 55px;
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li + li {
|
||||
border-top: 1px solid color-mix(in oklch, var(--primary) 17%, var(--border));
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-icon {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-icon svg {
|
||||
width: 17px;
|
||||
height: 17px;
|
||||
stroke-width: 1.8;
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-label {
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--muted-foreground, #5b687a);
|
||||
font-size: 12px;
|
||||
font-weight: 680;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.assistant-dispatched-control-value {
|
||||
max-width: 112px;
|
||||
overflow: hidden;
|
||||
color: var(--heading-foreground, #26364b);
|
||||
font-size: 12px;
|
||||
font-weight: 850;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* The function-mode strip is now the only homepage header. */
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel,
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .main-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-output-shell,
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .v2-output-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher,
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .main-panel > .v2-mode-rail .v2-module-switcher {
|
||||
width: auto;
|
||||
min-width: max-content;
|
||||
min-height: 0;
|
||||
position: static;
|
||||
inset: auto;
|
||||
z-index: auto;
|
||||
display: block;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
@media (min-width: 901px) {
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync.assistant-drawer-open .v2-operation-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .main-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-output-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync .v2-operation-shell {
|
||||
top: 108px;
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls {
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -502,6 +502,282 @@
|
||||
followLatest(true);
|
||||
}
|
||||
|
||||
const assistantControlLabels = {
|
||||
"climate.set_power": "空调电源",
|
||||
"climate.set_temperature": "空调温度",
|
||||
"climate.set_fan_level": "空调风量",
|
||||
"climate.set_outlet_mode": "出风模式",
|
||||
"climate.set_control_mode": "空调控制方式",
|
||||
"climate.set_cycle_mode": "空调循环",
|
||||
"interior_light.set_indoor_power": "室内灯",
|
||||
"interior_light.set_reading_power": "阅读灯",
|
||||
"ambient_light.set_power": "氛围灯",
|
||||
"ambient_light.set_color_index": "氛围灯颜色",
|
||||
"ambient_light.set_brightness_level": "氛围灯亮度",
|
||||
"ambient_light.set_illumination": "氛围灯颜色与亮度",
|
||||
"ambient_light.set_mode": "氛围灯模式",
|
||||
"roof.set_shade_motion": "遮阳帘",
|
||||
"roof.set_shade_position": "遮阳帘位置",
|
||||
"window.set_motion": "车窗",
|
||||
"window.set_position": "车窗位置",
|
||||
"door.set_rear_lock": "后排门锁",
|
||||
"door.set_vehicle_lock": "整车门锁",
|
||||
"infrared.emit": "红外外设",
|
||||
};
|
||||
|
||||
function assistantControlCategory(operation = {}, fallback = "控制指令") {
|
||||
const capability = String(operation.capability || "");
|
||||
if (capability === "infrared.emit") {
|
||||
const deviceName = String(
|
||||
operation.target?.name || operation.target?.device || "红外外设",
|
||||
).trim();
|
||||
return deviceName || "红外外设";
|
||||
}
|
||||
if (assistantControlLabels[capability]) return assistantControlLabels[capability];
|
||||
if (capability.startsWith("seat.")) {
|
||||
const target = String(operation.target?.zone || operation.target?.instance || "")
|
||||
.replaceAll("_", "-");
|
||||
return `${["passenger", "front-right"].includes(target) ? "副驾" : "主驾"}座椅`;
|
||||
}
|
||||
if (capability.startsWith("outlet.")) return "空调风口";
|
||||
return String(fallback || capability || "控制指令");
|
||||
}
|
||||
|
||||
function fallbackAssistantControlCommands(result = {}) {
|
||||
return (Array.isArray(result.validated_control_commands)
|
||||
? result.validated_control_commands
|
||||
: [])
|
||||
.map((presentation) => {
|
||||
const operation = presentation?.operation || {};
|
||||
const command = String(presentation?.actual_command || "").trim();
|
||||
if (!command) return null;
|
||||
const capability = String(operation.capability || "");
|
||||
const target = operation.target || {};
|
||||
const targetValue = target.zone || target.instance || target.device;
|
||||
const semanticArguments = [];
|
||||
if (targetValue != null && String(targetValue).trim()) {
|
||||
semanticArguments.push(`target=${String(targetValue).replaceAll("_", "-")}`);
|
||||
}
|
||||
Object.entries(operation.parameters || {})
|
||||
.sort(([left], [right]) => left.localeCompare(right))
|
||||
.forEach(([key, value]) => semanticArguments.push(`${key}=${String(value)}`));
|
||||
return {
|
||||
category: assistantControlCategory(operation, capability),
|
||||
channel: String(presentation?.channel || ""),
|
||||
command,
|
||||
deliveryState: "not_submitted",
|
||||
operation,
|
||||
operationRef: String(operation.id || ""),
|
||||
semantic: `${capability || "control"}(${semanticArguments.join(", ")})`,
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function assistantDispatchedCommands(result = {}) {
|
||||
const projections = Array.isArray(result.validated_control_commands)
|
||||
? result.validated_control_commands
|
||||
: [];
|
||||
const projectionByRef = new Map(projections.map((presentation) => [
|
||||
String(presentation?.operation?.id || ""),
|
||||
presentation?.operation || {},
|
||||
]));
|
||||
let commands = [];
|
||||
try {
|
||||
const sharedCommands = window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result);
|
||||
if (Array.isArray(sharedCommands)) commands = sharedCommands;
|
||||
} catch (_error) {
|
||||
commands = [];
|
||||
}
|
||||
if (!commands.length) return fallbackAssistantControlCommands(result);
|
||||
return commands.map((item, index) => {
|
||||
const operation = projectionByRef.get(String(item.operationRef || ""))
|
||||
|| projections[index]?.operation
|
||||
|| {};
|
||||
return {
|
||||
...item,
|
||||
category: assistantControlCategory(operation, item.category),
|
||||
operation,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function localizedAssistantControlValue(value) {
|
||||
const normalized = String(value ?? "").trim().toLowerCase();
|
||||
return {
|
||||
on: "开启",
|
||||
open: "开启",
|
||||
true: "开启",
|
||||
off: "关闭",
|
||||
close: "关闭",
|
||||
false: "关闭",
|
||||
half: "一半",
|
||||
stop: "停止",
|
||||
lock: "上锁",
|
||||
unlock: "解锁",
|
||||
auto: "自动",
|
||||
manual: "手动",
|
||||
internal: "内循环",
|
||||
external: "外循环",
|
||||
}[normalized] || String(value ?? "").trim();
|
||||
}
|
||||
|
||||
function assistantControlValue(item = {}) {
|
||||
const parameters = item.operation?.parameters || {};
|
||||
const numericValue = (value) => (
|
||||
value !== null && value !== undefined && value !== "" && Number.isFinite(Number(value))
|
||||
? Number(value)
|
||||
: null
|
||||
);
|
||||
const temperature = numericValue(parameters.temperature_c);
|
||||
if (temperature !== null) return `${temperature}°C`;
|
||||
const position = numericValue(parameters.position_pct ?? parameters.shade_position_pct);
|
||||
if (position !== null) return `${position}%`;
|
||||
const illumination = String(parameters.color_hex || "").trim();
|
||||
const brightnessPercent = numericValue(parameters.brightness_pct);
|
||||
if (illumination && brightnessPercent !== null) return `${illumination} · ${brightnessPercent}%`;
|
||||
if (illumination) return illumination;
|
||||
if (brightnessPercent !== null) return `${brightnessPercent}%`;
|
||||
const brightness = numericValue(parameters.brightness_level);
|
||||
if (brightness !== null) return `${brightness}级`;
|
||||
const level = numericValue(parameters.level ?? parameters.fan_level);
|
||||
if (level !== null) return `${level}档`;
|
||||
const state = parameters.motion
|
||||
?? parameters.state
|
||||
?? parameters.power
|
||||
?? parameters.action
|
||||
?? parameters.mode;
|
||||
if (state !== null && state !== undefined && String(state).trim()) {
|
||||
return localizedAssistantControlValue(state);
|
||||
}
|
||||
const commandTail = String(item.command || "").trim().split(/\s+/).at(-1);
|
||||
return localizedAssistantControlValue(commandTail) || String(item.command || "控制");
|
||||
}
|
||||
|
||||
function assistantControlIcon(item = {}) {
|
||||
const capability = String(item.operation?.capability || "");
|
||||
const category = String(item.category || "");
|
||||
if (capability.startsWith("climate.") || category.includes("空调")) return "thermometer";
|
||||
if (capability.includes("reading") || category.includes("阅读灯")) return "book-open";
|
||||
if (capability.includes("light") || category.includes("灯")) return "lightbulb";
|
||||
if (capability.startsWith("roof.") || category.includes("遮阳帘")) return "panel-top";
|
||||
if (capability.startsWith("window.") || category.includes("车窗")) return "panels-top-left";
|
||||
if (capability.startsWith("seat.") || category.includes("座椅")) return "armchair";
|
||||
if (capability.startsWith("door.") || category.includes("门锁")) return "lock-keyhole";
|
||||
if (capability === "infrared.emit") return "radio-tower";
|
||||
return "binary";
|
||||
}
|
||||
|
||||
function appendDispatchedControls(message, result = {}) {
|
||||
if (!message?.body || !result || typeof result !== "object") return;
|
||||
const previous = message.dispatchedControlResult || {};
|
||||
const mergedResult = { ...previous, ...result };
|
||||
if (!Array.isArray(result.validated_control_commands)
|
||||
&& Array.isArray(previous.validated_control_commands)) {
|
||||
mergedResult.validated_control_commands = previous.validated_control_commands;
|
||||
}
|
||||
message.dispatchedControlResult = mergedResult;
|
||||
const commands = assistantDispatchedCommands(mergedResult);
|
||||
if (!commands.length) return;
|
||||
|
||||
const signature = JSON.stringify(commands.map((item) => [
|
||||
item.operationRef,
|
||||
item.category,
|
||||
item.command,
|
||||
item.semantic,
|
||||
item.deliveryState,
|
||||
item.deliveryLabel,
|
||||
item.deliveryDetail,
|
||||
]));
|
||||
const vueRenderer = window.CognitiveAgentVueUI?.renderDispatchedControls;
|
||||
if (typeof vueRenderer === "function") {
|
||||
let host = $(".assistant-dispatched-controls-vue-host", message.body);
|
||||
if (host?.dataset.commandSignature === signature) return;
|
||||
if (!host) {
|
||||
host = document.createElement("div");
|
||||
host.className = "assistant-dispatched-controls-vue-host";
|
||||
message.body.append(host);
|
||||
}
|
||||
const viewCommands = commands.map((item, index) => ({
|
||||
key: item.operationRef || `${item.semantic || item.command}-${index}`,
|
||||
category: String(item.category || "控制指令"),
|
||||
value: assistantControlValue(item),
|
||||
icon: assistantControlIcon(item),
|
||||
semantic: String(item.semantic || ""),
|
||||
command: String(item.command || ""),
|
||||
deliveryState: String(item.deliveryState || "not_submitted"),
|
||||
deliveryLabel: String(item.deliveryLabel || "未下发"),
|
||||
deliveryDetail: String(item.deliveryDetail || ""),
|
||||
}));
|
||||
if (vueRenderer(host, viewCommands)) {
|
||||
host.dataset.commandSignature = signature;
|
||||
message.article?.classList.add("assistant-message-with-dispatched-controls");
|
||||
followLatest(true);
|
||||
return;
|
||||
}
|
||||
host.remove();
|
||||
}
|
||||
let card = $(".assistant-dispatched-controls", message.body);
|
||||
if (card?.dataset.commandSignature === signature) return;
|
||||
if (!card) {
|
||||
card = document.createElement("details");
|
||||
card.className = "assistant-dispatched-controls";
|
||||
card.open = true;
|
||||
const summary = document.createElement("summary");
|
||||
const headingIcon = document.createElement("span");
|
||||
headingIcon.className = "assistant-dispatched-controls-heading-icon";
|
||||
headingIcon.innerHTML = '<i data-lucide="list-tree"></i>';
|
||||
const heading = document.createElement("b");
|
||||
heading.textContent = "下发控制指令";
|
||||
const count = document.createElement("strong");
|
||||
count.className = "assistant-dispatched-controls-count";
|
||||
const chevron = document.createElement("i");
|
||||
chevron.className = "assistant-dispatched-controls-chevron";
|
||||
chevron.setAttribute("data-lucide", "chevron-down");
|
||||
summary.append(headingIcon, heading, count, chevron);
|
||||
const list = document.createElement("ol");
|
||||
list.className = "assistant-dispatched-controls-list";
|
||||
card.append(summary, list);
|
||||
message.body.append(card);
|
||||
}
|
||||
|
||||
card.dataset.commandSignature = signature;
|
||||
const count = $(".assistant-dispatched-controls-count", card);
|
||||
const list = $(".assistant-dispatched-controls-list", card);
|
||||
if (count) count.textContent = String(commands.length);
|
||||
if (!list) return;
|
||||
list.replaceChildren();
|
||||
commands.forEach((item) => {
|
||||
const row = document.createElement("li");
|
||||
row.dataset.deliveryState = String(item.deliveryState || "not_submitted");
|
||||
const titleParts = [];
|
||||
if (item.semantic) titleParts.push(`语义控制指令:${item.semantic}`);
|
||||
if (item.command) titleParts.push(`实际控制指令:${item.command}`);
|
||||
if (item.deliveryLabel) {
|
||||
titleParts.push(`下发状态:${item.deliveryLabel}${item.deliveryDetail ? `(${item.deliveryDetail})` : ""}`);
|
||||
}
|
||||
row.title = titleParts.join("\n");
|
||||
|
||||
const icon = document.createElement("span");
|
||||
icon.className = "assistant-dispatched-control-icon";
|
||||
icon.innerHTML = `<i data-lucide="${assistantControlIcon(item)}"></i>`;
|
||||
const label = document.createElement("span");
|
||||
label.className = "assistant-dispatched-control-label";
|
||||
label.textContent = String(item.category || "控制指令");
|
||||
const value = document.createElement("strong");
|
||||
value.className = "assistant-dispatched-control-value";
|
||||
value.textContent = assistantControlValue(item);
|
||||
const accessibleCommand = document.createElement("small");
|
||||
accessibleCommand.className = "visually-hidden";
|
||||
accessibleCommand.textContent = titleParts.join(";");
|
||||
row.append(icon, label, value, accessibleCommand);
|
||||
list.append(row);
|
||||
});
|
||||
message.article?.classList.add("assistant-message-with-dispatched-controls");
|
||||
window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } });
|
||||
followLatest(true);
|
||||
}
|
||||
|
||||
function appendModeCreatedState(message, profile, { updated = false } = {}) {
|
||||
if (!message?.body || !profile?.id) return;
|
||||
const selector = `[data-assistant-mode-created="${CSS.escape(String(profile.id))}"]`;
|
||||
@@ -921,6 +1197,7 @@
|
||||
"cognitive-agent:validated-can-commands",
|
||||
{ detail: event.payload },
|
||||
));
|
||||
appendDispatchedControls(message, event.payload);
|
||||
return;
|
||||
}
|
||||
if (event.type === "answer.delta") {
|
||||
@@ -952,6 +1229,7 @@
|
||||
);
|
||||
if (event.payload) {
|
||||
window.CognitiveAgentConsole?.applySyncedResult?.(event.payload);
|
||||
appendDispatchedControls(message, event.payload);
|
||||
if (
|
||||
event.payload.mode_profile_draft
|
||||
&& event.payload.orchestration_route === "mode_profile_create_preview"
|
||||
@@ -1165,6 +1443,44 @@
|
||||
}
|
||||
}
|
||||
|
||||
function announceModeExecution(profile, result = null, { command = "", error = null } = {}) {
|
||||
const modeName = String(profile?.name || profile?.id || "功能模式");
|
||||
const commandText = String(command || `进入${modeName}`).trim();
|
||||
const status = String(result?.status || "");
|
||||
const dryRun = result?.execution_mode === "dry_run";
|
||||
const waitingForFeedback = result?.execution_outcome === "submitted_unverified";
|
||||
setDrawerOpen(true);
|
||||
createMessage("user", commandText);
|
||||
|
||||
let content = `${modeName}执行未完成,请查看控制指令状态。`;
|
||||
let statusText = "功能模式执行未完成";
|
||||
let statusState = "failed";
|
||||
if (error) {
|
||||
content = `${modeName}执行失败:${error.message || "请稍后重试。"}`;
|
||||
statusText = "功能模式执行失败";
|
||||
} else if (status === "succeeded") {
|
||||
content = dryRun
|
||||
? `已完成模拟编排:${commandText}。`
|
||||
: waitingForFeedback
|
||||
? `已提交执行:${commandText},正在等待车辆状态确认。`
|
||||
: `已完成执行:${commandText}。`;
|
||||
statusText = dryRun
|
||||
? "功能模式模拟编排已完成"
|
||||
: waitingForFeedback
|
||||
? "模式指令已提交,等待车辆状态确认"
|
||||
: "功能模式执行已完成";
|
||||
statusState = "success";
|
||||
} else if (status === "partially_succeeded") {
|
||||
content = `${modeName}仅部分完成,请查看未完成的控制指令。`;
|
||||
statusText = "功能模式部分完成";
|
||||
}
|
||||
|
||||
const message = createMessage("assistant", content);
|
||||
setActionStatus(message, statusText, statusState);
|
||||
if (result) appendDispatchedControls(message, result);
|
||||
persistMessages();
|
||||
}
|
||||
|
||||
function renderInitialMessages() {
|
||||
const container = $("#assistantMessages");
|
||||
if (!container) return;
|
||||
@@ -1221,6 +1537,7 @@
|
||||
open: () => setDrawerOpen(true),
|
||||
close: () => setDrawerOpen(false),
|
||||
startModeCreation,
|
||||
announceModeExecution,
|
||||
announceModeCreated(profile) {
|
||||
if (!profile?.id) return;
|
||||
setDrawerOpen(true);
|
||||
|
||||
@@ -9637,6 +9637,87 @@ body.device-power-drawer-open {
|
||||
border-left-color: color-mix(in oklch, var(--primary) 52%, var(--border));
|
||||
}
|
||||
|
||||
/* Unified homepage header: panel controls and function modes share one row. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel {
|
||||
grid-template-rows: auto minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-output-shell {
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher {
|
||||
width: auto;
|
||||
min-width: max-content;
|
||||
min-height: 0;
|
||||
position: static;
|
||||
inset: auto;
|
||||
z-index: auto;
|
||||
display: block;
|
||||
grid-column: auto;
|
||||
grid-row: auto;
|
||||
gap: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
overflow: visible;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
box-shadow: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail.is-collapsed > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
@media (max-width: 980px) {
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto auto minmax(0, 1fr) auto;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail .mode-rail-actions {
|
||||
grid-column: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar,
|
||||
.console-v2 .main-panel > .v2-mode-rail.is-collapsed > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-template-rows: auto auto;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .main-panel > .v2-mode-rail .v2-module-switcher {
|
||||
grid-column: 1 / -1;
|
||||
grid-row: 1;
|
||||
justify-self: start;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail .mode-rail-identity {
|
||||
grid-column: 1;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail > .mode-rail-bar > .mode-rail-content {
|
||||
grid-column: 2;
|
||||
grid-row: 2;
|
||||
}
|
||||
|
||||
.console-v2 .main-panel > .v2-mode-rail .mode-rail-actions {
|
||||
grid-column: 3;
|
||||
grid-row: 2;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 1120px) {
|
||||
.console-v2 .v2-mode-rail > .mode-rail-bar {
|
||||
grid-template-columns: auto minmax(150px, 1fr) auto;
|
||||
|
||||
@@ -373,6 +373,19 @@
|
||||
if (new Set(["driver", "passenger", "rear"]).has(target) && temperature !== null) {
|
||||
return command("空调温度", `ac temp ${target} ${temperature}`);
|
||||
}
|
||||
const vehicleZones = new Set([
|
||||
"vehicle",
|
||||
"front-row",
|
||||
"rear-row",
|
||||
"rear-left",
|
||||
"rear-right",
|
||||
]);
|
||||
if (vehicleZones.has(target) && temperature !== null) {
|
||||
return command(
|
||||
"空调温度",
|
||||
`Climate_Control_064_04(${target}=${temperature}°C)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (capability === "climate.set_control_mode") {
|
||||
const mode = String(parameters.mode ?? parameters.control_mode ?? "");
|
||||
@@ -466,6 +479,22 @@
|
||||
const level = boundedInteger(parameters.brightness_level, 1, 5);
|
||||
if (level !== null) return command("氛围灯", `light ambient brightness ${level}`);
|
||||
}
|
||||
if (capability === "ambient_light.set_illumination") {
|
||||
const color = String(parameters.color_hex || "").trim().toUpperCase();
|
||||
const brightness = boundedInteger(parameters.brightness_pct, 0, 100);
|
||||
if (/^#[0-9A-F]{6}$/.test(color) && brightness !== null) {
|
||||
return command(
|
||||
"氛围灯颜色与亮度",
|
||||
`Ambient_RGB_Control_32B_2B(color=${color}, brightness=${brightness}%)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (capability === "ambient_light.set_mode") {
|
||||
const mode = String(parameters.mode || "").trim();
|
||||
if (new Set(["camping", "rest", "cinema", "meditation", "romantic", "off", "on"]).has(mode)) {
|
||||
return command("氛围灯模式", `Ambient_Mode_Control_496_04(mode=${mode})`);
|
||||
}
|
||||
}
|
||||
if (capability === "roof.set_shade_motion") {
|
||||
const motion = String(parameters.motion || "");
|
||||
if (new Set(["open", "close", "half", "stop"]).has(motion)) {
|
||||
@@ -526,13 +555,39 @@
|
||||
|| telemetry.feedback_observed === true
|
||||
|| telemetry.physical_effect_confirmed === true
|
||||
);
|
||||
const failureMessage = String(execution?.message || "").trim();
|
||||
const executionFailed = execution?.status === "failed"
|
||||
|| telemetry.verification_status === "failed_or_blocked";
|
||||
const safetyBlocked = executionFailed && !submitted && (
|
||||
/no fresh valid 0x41a shade position/i.test(failureMessage)
|
||||
|| /refusing (?:blind )?motion/i.test(failureMessage)
|
||||
|| /thermal protection/i.test(failureMessage)
|
||||
);
|
||||
const deliveryState = safetyBlocked
|
||||
? "blocked"
|
||||
: executionFailed
|
||||
? "failed"
|
||||
: submitted
|
||||
? "submitted"
|
||||
: "not_submitted";
|
||||
const deliveryLabel = safetyBlocked
|
||||
? "安全阻止"
|
||||
: executionFailed
|
||||
? "执行失败"
|
||||
: submitted
|
||||
? "下发"
|
||||
: "未下发";
|
||||
const deliveryDetail = /no fresh valid 0x41a shade position/i.test(failureMessage)
|
||||
? "未收到遮阳帘 CAN1 / 0x41A 实时位置,无法判断移动方向,系统已阻止盲动"
|
||||
: failureMessage;
|
||||
const transportLabel = capability === "infrared.emit"
|
||||
? "红外 TCP"
|
||||
: result.control_transport === "wireless_mqtt" ? "无线 CAN" : "有线 CAN";
|
||||
return {
|
||||
deliveryState: submitted ? "submitted" : "not_submitted",
|
||||
deliveryLabel: submitted ? "下发" : "未下发",
|
||||
deliveryState,
|
||||
deliveryLabel,
|
||||
deliveryTransport: transportLabel,
|
||||
...(deliveryDetail ? { deliveryDetail } : {}),
|
||||
};
|
||||
};
|
||||
const commands = [];
|
||||
@@ -816,7 +871,7 @@
|
||||
<span>
|
||||
<b class="v2-can-command-channel">${escapeHtml(item.channel)}</b>
|
||||
${item.parseSource ? `<b class="v2-can-command-source" data-parse-source="${escapeHtml(item.parseSource)}">${parsedCommandSourceLabel(item.parseSource)}</b>` : ""}
|
||||
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml(`${item.deliveryTransport} · ${item.deliveryLabel}`)}">${escapeHtml(item.deliveryLabel)}</b>
|
||||
<b class="v2-can-command-delivery" data-delivery-state="${escapeHtml(item.deliveryState)}" title="${escapeHtml([item.deliveryTransport, item.deliveryLabel, item.deliveryDetail].filter(Boolean).join(" · "))}">${escapeHtml(item.deliveryLabel)}</b>
|
||||
<i class="v2-can-command-category">${escapeHtml(item.category)}</i>
|
||||
</span>
|
||||
<div class="v2-can-command-layer v2-can-command-semantic">
|
||||
@@ -1083,20 +1138,16 @@
|
||||
return operation;
|
||||
}
|
||||
|
||||
function buildModuleSwitcher(mainPanel) {
|
||||
function buildModuleSwitcher(mainPanel, modeRail) {
|
||||
const switcher = make("nav", "v2-module-switcher", "");
|
||||
switcher.id = "v2ModuleSwitcher";
|
||||
switcher.dataset.unifiedSync = "true";
|
||||
switcher.setAttribute("aria-label", "智能座舱远程控制与同步状态");
|
||||
switcher.setAttribute("aria-label", "页面板块展开与收起");
|
||||
switcher.innerHTML = `
|
||||
<div class="v2-unified-brand">
|
||||
<span class="v2-unified-brand-mark"><i data-lucide="waypoints"></i></span>
|
||||
<div><h1>511工程-智能座舱</h1></div>
|
||||
</div>
|
||||
<div class="v2-remote-control-state"><i data-lucide="radio-tower"></i><span>网页控制链路</span><strong>已启用</strong></div>
|
||||
<div class="v2-unified-live"><i></i><span>LIVE</span><b id="v2SyncClock">--:--:--</b></div>
|
||||
<nav class="v2-panel-control-dock" id="v2PanelControlDock" aria-label="页面板块展开与收起"></nav>`;
|
||||
mainPanel.prepend(switcher);
|
||||
const modeRailBar = $(".mode-rail-bar", modeRail);
|
||||
if (modeRailBar) modeRailBar.prepend(switcher);
|
||||
else mainPanel.prepend(switcher);
|
||||
const panelDock = $("#v2PanelControlDock", switcher);
|
||||
const sidebarToggle = $("#v2SidebarToggle");
|
||||
if (panelDock && sidebarToggle) panelDock.append(sidebarToggle);
|
||||
@@ -1231,6 +1282,11 @@
|
||||
return text;
|
||||
}
|
||||
|
||||
function ambientPowerState() {
|
||||
const value = $('[data-domain-panel="ambient_light"]')?.dataset.ambientPower;
|
||||
return value === "on" || value === "off" ? value : "unknown";
|
||||
}
|
||||
|
||||
function selectedTransportHasConnection() {
|
||||
const selected = $('.vehicle-transport-options [data-control-transport][aria-pressed="true"]')
|
||||
|| $('.vehicle-transport-options [data-control-transport].active');
|
||||
@@ -1340,15 +1396,15 @@
|
||||
rows: () => {
|
||||
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
||||
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
|
||||
const rawPower = normalizeDeviceReadout(readText("#ambientPower"));
|
||||
const power = rawPower === "-" ? "-" : /OFF|关闭/.test(rawPower) ? "关闭" : "开启";
|
||||
const powerState = ambientPowerState();
|
||||
const power = powerState === "on" ? "开启" : powerState === "off" ? "关闭" : "-";
|
||||
return [["开关状态", power], ["前排亮度", frontBrightness], ["前排颜色", frontColor], ["后排亮度", rearBrightness], ["后排颜色", rearColor]];
|
||||
},
|
||||
controls: () => {
|
||||
const [frontBrightness, rearBrightness] = splitReadout("#ambientBrightness");
|
||||
const [frontColor, rearColor] = splitReadout("#ambientColorIndex");
|
||||
return [
|
||||
selectControl("power", "开关状态", /OFF|关闭/.test(readText("#ambientPower")) ? "off" : "on", toggleOptions),
|
||||
selectControl("power", "开关状态", ambientPowerState() === "off" ? "off" : "on", toggleOptions),
|
||||
rangeControl("brightness", "全车亮度", frontBrightness === "-" ? rearBrightness : frontBrightness, 1, 5, 1, " 级"),
|
||||
selectControl("color", "全车颜色", String(clampControlNumber(frontColor === "-" ? rearColor : frontColor, 1, 127, 1)), colorIndexOptions),
|
||||
];
|
||||
@@ -2141,9 +2197,7 @@
|
||||
const sidebar = $(".v2-sidebar");
|
||||
const sidebarFooter = $(".v2-sidebar-footer", sidebar);
|
||||
const syncSidebar = make("section", "v2-sync-sidebar", `
|
||||
<div class="v2-sync-sidebar-overview">
|
||||
<div class="v2-sync-sidebar-heading"><h2>511工程-智能座舱</h2></div>
|
||||
</div>
|
||||
<div class="v2-sync-sidebar-overview"></div>
|
||||
<section class="v2-sync-log" aria-labelledby="v2SyncLogTitle">
|
||||
<header><div><h3 id="v2SyncLogTitle">运行日志 / 同步记录</h3></div><select id="v2LogFilter" aria-label="筛选同步日志"><option value="all">全部</option><option value="task">任务</option><option value="sync">同步</option><option value="extension">外设</option><option value="device">设备</option></select></header>
|
||||
<ol id="v2SyncLogList" aria-live="polite"></ol>
|
||||
@@ -2679,7 +2733,7 @@
|
||||
requestHeader,
|
||||
gestureDrawer,
|
||||
);
|
||||
buildModuleSwitcher(mainPanel);
|
||||
buildModuleSwitcher(mainPanel, modeRail);
|
||||
buildOutput(mainPanel, modeRail);
|
||||
buildSyncControlLayer();
|
||||
bindEvents();
|
||||
|
||||
@@ -10,23 +10,26 @@
|
||||
<link rel="icon" href="/icons/app-icon-192.png" />
|
||||
<title>511工程-智能座舱</title>
|
||||
<link rel="stylesheet" href="/styles.css?v=dual-seat-23" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-68" />
|
||||
<link rel="stylesheet" href="/console-v2.css?v=delivery-state-69" />
|
||||
<link rel="stylesheet" href="/self-extension.css?v=gscu-tcp-43" />
|
||||
<link rel="stylesheet" href="/speech-input.css?v=sensevoice-7" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-26" />
|
||||
<link rel="stylesheet" href="/assistant-chat.css?v=codex-assistant-28" />
|
||||
<link rel="stylesheet" href="/energy-monitor.css?v=energy-monitor-4" />
|
||||
<link rel="stylesheet" href="/ui-consistency.css?v=spatial-ui-4" />
|
||||
<script src="/vendor/lucide/0.468.0/lucide.min.js" defer></script>
|
||||
<script src="/vendor/vue/3.5.21/vue.runtime.global.prod.js" defer></script>
|
||||
<script src="/vue-ui.js?v=vue-presentation-3" defer></script>
|
||||
<script src="/vendor/three/0.147.0/three.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/fflate.min.js" defer></script>
|
||||
<script src="/vendor/three/0.147.0/FBXLoader.js" defer></script>
|
||||
<script src="/network.js?v=android-pad-2" defer></script>
|
||||
<script src="/app.js?v=dual-seat-131" defer></script>
|
||||
<script src="/app.js?v=dual-seat-135" defer></script>
|
||||
<script src="/speech-input.js?v=sensevoice-13" defer></script>
|
||||
<script type="module" src="/gesture-control.js?v=android-pad-9"></script>
|
||||
<script src="/pwa.js?v=android-pad-3" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-66" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-43" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-27" defer></script>
|
||||
<script src="/console-v2.js?v=local-first-70" defer></script>
|
||||
<script src="/self-extension.js?v=gscu-tcp-44" defer></script>
|
||||
<script src="/assistant-chat.js?v=codex-assistant-32" defer></script>
|
||||
<script src="/energy-monitor.js?v=energy-monitor-4" defer></script>
|
||||
</head>
|
||||
<body class="console-v2">
|
||||
|
||||
@@ -666,7 +666,8 @@
|
||||
<span class="v2-extension-pet-mouth-closed" aria-hidden="true"></span>
|
||||
</button>
|
||||
</aside>`;
|
||||
moduleSwitcher.insertAdjacentElement("afterend", root);
|
||||
const homepageHeader = moduleSwitcher.closest(".v2-mode-rail") || moduleSwitcher;
|
||||
homepageHeader.insertAdjacentElement("afterend", root);
|
||||
|
||||
buildDialogs();
|
||||
return true;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v282";
|
||||
const CACHE_VERSION = "cognitive-agent-pad-v290";
|
||||
const SHELL_CACHE = `${CACHE_VERSION}-shell`;
|
||||
const RUNTIME_CACHE = `${CACHE_VERSION}-runtime`;
|
||||
const APP_SHELL = [
|
||||
@@ -6,26 +6,29 @@ const APP_SHELL = [
|
||||
"/index.html",
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/network.js?v=android-pad-2",
|
||||
"/app.js?v=dual-seat-131",
|
||||
"/app.js?v=dual-seat-135",
|
||||
"/gesture-control.js?v=android-pad-9",
|
||||
"/pwa.js?v=android-pad-3",
|
||||
"/console-v2.css?v=delivery-state-68",
|
||||
"/console-v2.js?v=local-first-66",
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/console-v2.js?v=local-first-70",
|
||||
"/self-extension.css?v=gscu-tcp-43",
|
||||
"/self-extension.js?v=gscu-tcp-43",
|
||||
"/self-extension.js?v=gscu-tcp-44",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
"/speech-input.js?v=sensevoice-13",
|
||||
"/speech-audio-worklet.js?v=sensevoice-5",
|
||||
"/assistant-chat.css?v=codex-assistant-26",
|
||||
"/assistant-chat.js?v=codex-assistant-27",
|
||||
"/assistant-chat.css?v=codex-assistant-28",
|
||||
"/assistant-chat.js?v=codex-assistant-32",
|
||||
"/energy-monitor.css?v=energy-monitor-4",
|
||||
"/energy-monitor.js?v=energy-monitor-4",
|
||||
"/ui-consistency.css?v=spatial-ui-4",
|
||||
"/vue-ui.js?v=vue-presentation-3",
|
||||
"/assets/pets/self-extension-cat.png?v=3",
|
||||
"/assets/pets/self-extension-cat-entrance.png?v=3",
|
||||
"/manifest.webmanifest",
|
||||
"/icons/app-icon-192.png",
|
||||
"/icons/app-icon-512.png",
|
||||
"/vendor/lucide/0.468.0/lucide.min.js",
|
||||
"/vendor/vue/3.5.21/vue.runtime.global.prod.js",
|
||||
"/vendor/three/0.147.0/three.min.js",
|
||||
"/vendor/three/0.147.0/fflate.min.js",
|
||||
"/vendor/three/0.147.0/FBXLoader.js",
|
||||
|
||||
@@ -0,0 +1,275 @@
|
||||
/* Shared spatial UI contract --------------------------------------------
|
||||
Keep geometry consistent across legacy modules without changing their
|
||||
domain-specific colors, content density, or interaction behavior.
|
||||
|
||||
Hierarchy: control 10px -> item 12px -> section 16px -> panel 18px ->
|
||||
modal 22px. All modules consume the same semantic color tokens. */
|
||||
:root {
|
||||
--ui-control-height-compact: 30px;
|
||||
--ui-control-height: 38px;
|
||||
--ui-control-size-icon: 36px;
|
||||
--ui-radius-control-inner: calc(var(--radius) - 2px);
|
||||
--ui-radius-control: var(--radius);
|
||||
--ui-radius-item: calc(var(--radius) + 2px);
|
||||
--ui-radius-section: calc(var(--radius) + 6px);
|
||||
--ui-radius-panel: calc(var(--radius) + 8px);
|
||||
--ui-radius-modal: calc(var(--radius) + 12px);
|
||||
--ui-space-card: 10px;
|
||||
--ui-border-subtle: 1px solid var(--border);
|
||||
--ui-shadow-item: 0 5px 14px color-mix(in oklch, var(--foreground) 6%, transparent);
|
||||
--ui-shadow-panel:
|
||||
0 14px 32px color-mix(in oklch, var(--foreground) 8%, transparent),
|
||||
inset 0 1px 0 color-mix(in oklch, var(--card) 78%, transparent);
|
||||
--ui-shadow-modal:
|
||||
0 28px 72px color-mix(in oklch, var(--foreground) 22%, transparent),
|
||||
0 4px 18px color-mix(in oklch, var(--primary) 8%, transparent),
|
||||
inset 0 1px 0 color-mix(in oklch, var(--card) 84%, transparent);
|
||||
--ui-focus-ring: 0 0 0 3px color-mix(in oklch, var(--ring) 22%, transparent);
|
||||
}
|
||||
|
||||
/* Top-level workspace surfaces share one frame. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.v2-operation-shell,
|
||||
.joint-twin-shell > .twin-band,
|
||||
#v2DevicePanel,
|
||||
#v2ExtensionPanel .v2-extension-sync-mount
|
||||
) {
|
||||
border: var(--ui-border-subtle);
|
||||
border-radius: var(--ui-radius-panel);
|
||||
box-shadow: var(--ui-shadow-panel);
|
||||
}
|
||||
|
||||
/* The operation shell owns the frame; its active view must not draw a
|
||||
second border directly inside the same spatial boundary. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-operation-shell > .v2-natural-panel {
|
||||
border: 0;
|
||||
border-radius: inherit;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* Modal surfaces and their backdrops use one elevation level. */
|
||||
:is(
|
||||
.mode-gallery-backdrop .mode-gallery,
|
||||
.mode-editor-backdrop .mode-editor,
|
||||
.energy-monitor-layer .energy-monitor-page
|
||||
) {
|
||||
border: 1px solid color-mix(in oklch, var(--primary) 18%, var(--border));
|
||||
border-radius: var(--ui-radius-modal);
|
||||
box-shadow: var(--ui-shadow-modal);
|
||||
}
|
||||
|
||||
.mode-gallery-backdrop,
|
||||
.mode-editor-backdrop,
|
||||
.energy-monitor-layer {
|
||||
background: color-mix(in oklch, var(--foreground) 42%, transparent);
|
||||
-webkit-backdrop-filter: blur(14px);
|
||||
backdrop-filter: blur(14px);
|
||||
}
|
||||
|
||||
/* Section containers sit between panels and individual cards. */
|
||||
.v2-sync-log,
|
||||
.mode-gallery-group,
|
||||
.energy-trend-card,
|
||||
.energy-devices-card {
|
||||
border-radius: var(--ui-radius-section);
|
||||
}
|
||||
|
||||
/* Repeated content cards share radius and elevation while retaining their
|
||||
own semantic state colors and specialized internal layouts. */
|
||||
.v2-synced-device,
|
||||
.v2-extension-gateway,
|
||||
.v2-nfc-inline,
|
||||
.mode-gallery-card,
|
||||
.mode-editor-flow,
|
||||
.mode-editor-command-card,
|
||||
.energy-kpi {
|
||||
border-radius: var(--ui-radius-item);
|
||||
box-shadow: var(--ui-shadow-item);
|
||||
}
|
||||
|
||||
.energy-kpi-primary {
|
||||
box-shadow: 0 8px 20px color-mix(in oklch, var(--primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.v2-extension-gateway,
|
||||
.v2-nfc-inline {
|
||||
padding: var(--ui-space-card);
|
||||
}
|
||||
|
||||
/* Sidebar cards and their nested segmented controls preserve a clear
|
||||
parent/child radius relationship. */
|
||||
.console-v2 :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel
|
||||
) {
|
||||
border-radius: var(--ui-radius-item);
|
||||
}
|
||||
|
||||
/* The compact-sidebar rules in console-v2.css intentionally use highly
|
||||
specific selectors. Keep all peer cards on the shared item radius even
|
||||
when that layout is active. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync :is(
|
||||
.v2-sync-transport-field,
|
||||
.v2-sync-execution-field,
|
||||
.v2-can-command-panel
|
||||
) {
|
||||
border-radius: var(--ui-radius-item) !important;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync .v2-sync-log {
|
||||
border-radius: var(--ui-radius-section) !important;
|
||||
}
|
||||
|
||||
.console-v2 :is(.segmented, .twin-selector) {
|
||||
gap: 4px;
|
||||
padding: 3px;
|
||||
background: var(--muted);
|
||||
border: var(--ui-border-subtle);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
.console-v2 .segmented > :is(.segment, .mode-option),
|
||||
.console-v2 .twin-selector > .twin-seat-button {
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
}
|
||||
|
||||
.console-v2 .segmented > .mode-option {
|
||||
min-height: var(--ui-control-height);
|
||||
height: var(--ui-control-height);
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.console-v2 .twin-selector > .twin-seat-button {
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
/* Primary header controls use one medium size. */
|
||||
.console-v2 #v2PanelControlDock > .v2-panel-control-button,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-mode-rail .mode-rail-actions > :is(
|
||||
#modeGalleryToggle,
|
||||
#saveModeToggle,
|
||||
#modeRailToggle
|
||||
) {
|
||||
min-height: var(--ui-control-height);
|
||||
height: var(--ui-control-height);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
#v2PanelControlDock > #v2SidebarToggle.v2-panel-control-button {
|
||||
border-radius: var(--ui-radius-control) !important;
|
||||
}
|
||||
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-mode-rail #modeRailToggle {
|
||||
width: var(--ui-control-height);
|
||||
min-width: var(--ui-control-height);
|
||||
}
|
||||
|
||||
/* Repeated inline actions use one compact size. */
|
||||
.console-v2 :is(
|
||||
.v2-device-adjust-toggle,
|
||||
.v2-extension-nfc-launch,
|
||||
.v2-extension-gateway > button,
|
||||
.v2-nfc-inline button
|
||||
),
|
||||
.energy-trend-tabs button {
|
||||
min-height: var(--ui-control-height-compact);
|
||||
height: var(--ui-control-height-compact);
|
||||
border-radius: var(--ui-radius-control-inner);
|
||||
}
|
||||
|
||||
/* Header icon actions are distinct from circular record/send actions. */
|
||||
.assistant-drawer-actions .context-reset,
|
||||
.assistant-drawer-close,
|
||||
.mode-gallery-close,
|
||||
.mode-editor-header > button,
|
||||
#energyMonitorClose {
|
||||
width: var(--ui-control-size-icon);
|
||||
min-width: var(--ui-control-size-icon);
|
||||
height: var(--ui-control-size-icon);
|
||||
min-height: var(--ui-control-size-icon);
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
/* Text fields align with medium controls and use the same focus treatment. */
|
||||
.mode-editor-field input,
|
||||
.mode-editor-field textarea {
|
||||
border-radius: var(--ui-radius-control);
|
||||
}
|
||||
|
||||
/* Reserve the right side for the microphone/send actions. Anchoring the hint
|
||||
from the content edge prevents the legacy right offsets from pushing it
|
||||
outside narrow assistant composers. */
|
||||
.console-v2.assistant-chat-enabled.v2-passive-display.v2-interactive-sync
|
||||
.v2-request-form .composer-hint {
|
||||
left: 16px !important;
|
||||
right: auto !important;
|
||||
bottom: 8px;
|
||||
max-width: calc(100% - 132px);
|
||||
}
|
||||
|
||||
/* Delivery states use the same semantic feedback language in the sidebar and
|
||||
in the compact assistant command card. */
|
||||
.console-v2.v2-passive-display.v2-interactive-sync
|
||||
.v2-can-command-delivery:is(
|
||||
[data-delivery-state="blocked"],
|
||||
[data-delivery-state="failed"]
|
||||
) {
|
||||
color: var(--destructive);
|
||||
background: color-mix(in oklch, var(--destructive) 10%, var(--card));
|
||||
border-color: color-mix(in oklch, var(--destructive) 28%, var(--border));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li:is(
|
||||
[data-delivery-state="blocked"],
|
||||
[data-delivery-state="failed"]
|
||||
) {
|
||||
background: color-mix(in oklch, var(--destructive) 4%, var(--card));
|
||||
}
|
||||
|
||||
.assistant-dispatched-controls-list > li:is(
|
||||
[data-delivery-state="blocked"],
|
||||
[data-delivery-state="failed"]
|
||||
) .assistant-dispatched-control-icon {
|
||||
color: var(--destructive);
|
||||
}
|
||||
|
||||
.console-v2 :is(
|
||||
#v2PanelControlDock > .v2-panel-control-button,
|
||||
.v2-mode-rail .mode-rail-actions > button,
|
||||
.segmented > button,
|
||||
.twin-selector > button,
|
||||
.v2-device-adjust-toggle,
|
||||
.v2-extension-nfc-launch,
|
||||
.v2-extension-gateway > button,
|
||||
.v2-nfc-inline button
|
||||
):focus-visible,
|
||||
.mode-gallery :is(button, input, textarea):focus-visible,
|
||||
.mode-editor :is(button, input, textarea):focus-visible,
|
||||
.energy-monitor-page button:focus-visible {
|
||||
outline: none;
|
||||
box-shadow: var(--ui-focus-ring);
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
:root {
|
||||
--ui-control-height: 40px;
|
||||
--ui-control-height-compact: 32px;
|
||||
--ui-control-size-icon: 38px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.v2-synced-device,
|
||||
.mode-gallery-card,
|
||||
.energy-kpi,
|
||||
.console-v2 button {
|
||||
transition-duration: 0.01ms !important;
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2018-present, Yuxi (Evan) You
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,176 @@
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
const VUE_VERSION = "3.5.21";
|
||||
const vue = window.Vue;
|
||||
const unavailable = {
|
||||
available: false,
|
||||
version: VUE_VERSION,
|
||||
renderDispatchedControls: () => false,
|
||||
renderModeChips: () => false,
|
||||
};
|
||||
if (!vue?.createApp || !vue?.h || !vue?.reactive) {
|
||||
window.CognitiveAgentVueUI = unavailable;
|
||||
return;
|
||||
}
|
||||
|
||||
const { Fragment, createApp, h, nextTick, reactive } = vue;
|
||||
const modeMounts = new WeakMap();
|
||||
const commandMounts = new WeakMap();
|
||||
const text = (value, fallback = "") => String(value ?? fallback);
|
||||
|
||||
function refreshIcons() {
|
||||
nextTick(() => window.lucide?.createIcons({ attrs: { "aria-hidden": "true" } }));
|
||||
}
|
||||
|
||||
function replaceReactiveArray(target, values) {
|
||||
target.splice(0, target.length, ...values.map((value) => ({ ...value })));
|
||||
}
|
||||
|
||||
function renderModeEntry(entry) {
|
||||
const key = text(entry.key || `${entry.type}-${entry.id || entry.label}`);
|
||||
if (entry.type === "label") {
|
||||
return h("span", {
|
||||
key,
|
||||
class: "mode-group-label",
|
||||
"data-mode-group-label": text(entry.id),
|
||||
}, text(entry.label));
|
||||
}
|
||||
if (entry.type === "divider") {
|
||||
return h("span", {
|
||||
key,
|
||||
class: "mode-group-divider",
|
||||
"data-mode-divider": text(entry.id),
|
||||
"aria-hidden": "true",
|
||||
});
|
||||
}
|
||||
const children = [
|
||||
h("i", {
|
||||
class: "mode-chip-icon",
|
||||
"data-lucide": text(entry.icon, "bookmark"),
|
||||
}),
|
||||
h("span", { class: "mode-chip-label" }, text(entry.label, "模式")),
|
||||
h("small", { class: "mode-chip-summary" }, text(entry.summary, "整车联动")),
|
||||
];
|
||||
if (entry.cycleLabel) {
|
||||
children.push(h("small", { class: "mode-cycle-badge" }, text(entry.cycleLabel)));
|
||||
}
|
||||
children.push(h("i", {
|
||||
class: "mode-chip-run",
|
||||
"data-lucide": "play",
|
||||
"data-mode-run": "true",
|
||||
title: `执行${text(entry.label, "模式")}`,
|
||||
}));
|
||||
return h("button", {
|
||||
key,
|
||||
type: "button",
|
||||
title: text(entry.title),
|
||||
"data-mode-profile-id": text(entry.id),
|
||||
"data-mode-command": text(entry.command),
|
||||
"data-mode-origin": text(entry.origin, "preset"),
|
||||
...(entry.behavior ? { "data-mode-behavior": text(entry.behavior) } : {}),
|
||||
"aria-label": text(entry.ariaLabel, `${text(entry.label, "模式")},点击执行`),
|
||||
"aria-pressed": "false",
|
||||
}, children);
|
||||
}
|
||||
|
||||
function createModeMount(container) {
|
||||
const state = reactive({ entries: [] });
|
||||
const app = createApp({
|
||||
name: "N7ModeChipRail",
|
||||
setup() {
|
||||
return () => h(Fragment, null, state.entries.map(renderModeEntry));
|
||||
},
|
||||
});
|
||||
app.mount(container);
|
||||
const mount = { app, state };
|
||||
modeMounts.set(container, mount);
|
||||
return mount;
|
||||
}
|
||||
|
||||
function renderModeChips(container, entries) {
|
||||
if (!(container instanceof Element) || !Array.isArray(entries)) return false;
|
||||
const mount = modeMounts.get(container) || createModeMount(container);
|
||||
replaceReactiveArray(mount.state.entries, entries);
|
||||
container.dataset.vueSurface = "mode-chips";
|
||||
refreshIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderCommandRow(command, index) {
|
||||
const semantic = text(command.semantic);
|
||||
const actual = text(command.command);
|
||||
const accessibleText = [
|
||||
semantic ? `语义控制指令:${semantic}` : "",
|
||||
actual ? `实际控制指令:${actual}` : "",
|
||||
command.deliveryLabel
|
||||
? `下发状态:${text(command.deliveryLabel)}${command.deliveryDetail ? `(${text(command.deliveryDetail)})` : ""}`
|
||||
: "",
|
||||
].filter(Boolean).join(";");
|
||||
return h("li", {
|
||||
key: text(command.key, `command-${index}`),
|
||||
title: accessibleText,
|
||||
"data-delivery-state": text(command.deliveryState, "not_submitted"),
|
||||
}, [
|
||||
h("span", { class: "assistant-dispatched-control-icon" }, [
|
||||
h("i", { "data-lucide": text(command.icon, "binary") }),
|
||||
]),
|
||||
h("span", { class: "assistant-dispatched-control-label" }, text(command.category, "控制指令")),
|
||||
h("strong", { class: "assistant-dispatched-control-value" }, text(command.value, "控制")),
|
||||
h("small", { class: "visually-hidden" }, accessibleText),
|
||||
]);
|
||||
}
|
||||
|
||||
function createCommandMount(container) {
|
||||
const state = reactive({ commands: [], open: true });
|
||||
const app = createApp({
|
||||
name: "N7DispatchedControlsCard",
|
||||
setup() {
|
||||
return () => h("details", {
|
||||
class: "assistant-dispatched-controls",
|
||||
open: state.open,
|
||||
onToggle(event) {
|
||||
state.open = event.currentTarget.open;
|
||||
},
|
||||
}, [
|
||||
h("summary", null, [
|
||||
h("span", { class: "assistant-dispatched-controls-heading-icon" }, [
|
||||
h("i", { "data-lucide": "list-tree" }),
|
||||
]),
|
||||
h("b", null, "下发控制指令"),
|
||||
h("strong", { class: "assistant-dispatched-controls-count" }, String(state.commands.length)),
|
||||
h("i", {
|
||||
class: "assistant-dispatched-controls-chevron",
|
||||
"data-lucide": "chevron-down",
|
||||
}),
|
||||
]),
|
||||
h("ol", { class: "assistant-dispatched-controls-list" }, state.commands.map(renderCommandRow)),
|
||||
]);
|
||||
},
|
||||
});
|
||||
app.mount(container);
|
||||
const mount = { app, state };
|
||||
commandMounts.set(container, mount);
|
||||
return mount;
|
||||
}
|
||||
|
||||
function renderDispatchedControls(container, commands) {
|
||||
if (!(container instanceof Element) || !Array.isArray(commands) || !commands.length) {
|
||||
return false;
|
||||
}
|
||||
const mount = commandMounts.get(container) || createCommandMount(container);
|
||||
replaceReactiveArray(mount.state.commands, commands);
|
||||
container.dataset.vueSurface = "dispatched-controls";
|
||||
refreshIcons();
|
||||
return true;
|
||||
}
|
||||
|
||||
document.documentElement.classList.add("vue-presentation-enabled");
|
||||
document.documentElement.dataset.vueVersion = VUE_VERSION;
|
||||
window.CognitiveAgentVueUI = Object.freeze({
|
||||
available: true,
|
||||
version: VUE_VERSION,
|
||||
renderDispatchedControls,
|
||||
renderModeChips,
|
||||
});
|
||||
}());
|
||||
@@ -114,10 +114,12 @@ FD+BRS。车机按通道、控制 ID 和帧类型推断预期反馈 ID。
|
||||
`/home/orin/project511/passenger_seat_firmware/Passenger_Seat_Controller_demo`
|
||||
中的副驾固件协议为准;
|
||||
入站状态必须是 11 位标准帧、经典 CAN、无 BRS、DLC 8。
|
||||
- CAN1 只以
|
||||
`/home/orin/project511/project-code/canfd/CAN矩阵表_filled.xlsx` 为准。
|
||||
例如 `0x42E` 颜色值只有 `0x01–0x1F` 有效,保留值不会显示为
|
||||
真实颜色。`0x388` 的四窗关好位按矩阵解析;byte 3–6 的四窗百分比另有
|
||||
- CAN1 以
|
||||
`/home/orin/project511/project-code/canfd/CAN矩阵表_filled.xlsx` 和已验证的
|
||||
N7 车型能力契约为准。`0x42E` 的颜色字段仍按矩阵的 7 位布局解码;通用矩阵
|
||||
将 `0x20–0x7F` 标记为预留,但 N7 的 127 色车型配置、控制白名单与实车 HIL
|
||||
均使用 `0x01–0x7F`,因此保留完整车型色号用于显示和反馈匹配,`0x00` 仍视为
|
||||
无效。该帧不包含 RGB 值,前端不得把色号伪装成精确颜色。`0x388` 的四窗关好位按矩阵解析;byte 3–6 的四窗百分比另有
|
||||
`project-code/canfd/window_native.txt` 实车变化轨迹和车端原始帧证据,
|
||||
因此只有数值落在 `0–100` 时才显示为开度。
|
||||
|
||||
|
||||
@@ -377,14 +377,18 @@ def decode_feedback(frame: CanFrame) -> dict[str, Any]:
|
||||
"rear_brightness_level": (
|
||||
rear_brightness_raw - 1 if 1 <= rear_brightness_raw <= 6 else None
|
||||
),
|
||||
# Sheet1 rows 1896-1897 define 0x01-0x1F as colors and
|
||||
# 0x20-0x7F as reserved. Do not expose a reserved raw value
|
||||
# as though it were a real cabin color.
|
||||
# 0x42E carries a 7-bit vehicle-profile color index. The
|
||||
# generic matrix labels 0x20-0x7F as reserved, but the N7
|
||||
# 127-color profile and its verified control/HIL contract use
|
||||
# the complete 0x01-0x7F range. Preserve that authoritative
|
||||
# index for display and feedback matching; zero remains the
|
||||
# matrix-defined invalid value. Exact RGB still cannot be
|
||||
# inferred from this frame.
|
||||
"front_color_index": (
|
||||
front_color if 1 <= front_color <= 0x1F else None
|
||||
front_color if 1 <= front_color <= 0x7F else None
|
||||
),
|
||||
"rear_color_index": (
|
||||
rear_color if 1 <= rear_color <= 0x1F else None
|
||||
rear_color if 1 <= rear_color <= 0x7F else None
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ from cognitive_agent.application.mode_profile_intent import ( # noqa: E402
|
||||
from cognitive_agent.application.seat_service import ( # noqa: E402
|
||||
LOCAL_CONTROL_BUDGET_MS,
|
||||
SeatDemoService,
|
||||
WIRELESS_MQTT_TRANSPORT,
|
||||
)
|
||||
from cognitive_agent.application.self_extension import ( # noqa: E402
|
||||
ExtensionIntegrationError,
|
||||
@@ -341,7 +342,11 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
vehicle_id,
|
||||
control_transport=control_transport,
|
||||
hardware_client_id=hardware_client_id,
|
||||
hardware_access_bypass=self._hardware_access_bypass_allowed(),
|
||||
hardware_access_bypass=(
|
||||
self._passive_state_access_bypass_allowed(
|
||||
control_transport
|
||||
)
|
||||
),
|
||||
)
|
||||
)
|
||||
except HardwareAccessRequiredError as exc:
|
||||
@@ -3645,6 +3650,23 @@ class SeatDemoHandler(BaseHTTPRequestHandler):
|
||||
and self._session_username() is not None
|
||||
)
|
||||
|
||||
def _passive_state_access_bypass_allowed(
|
||||
self,
|
||||
control_transport: str,
|
||||
) -> bool:
|
||||
"""Keep cached MQTT telemetry readable without widening control access.
|
||||
|
||||
The wireless state endpoints only consume the process-local snapshot
|
||||
populated by the persistent MQTT subscription; they never publish a
|
||||
frame or reserve the local CAN bench. Mutating control handlers must
|
||||
continue to call ``_hardware_access_bypass_allowed`` directly.
|
||||
"""
|
||||
|
||||
return (
|
||||
control_transport == WIRELESS_MQTT_TRANSPORT
|
||||
or self._hardware_access_bypass_allowed()
|
||||
)
|
||||
|
||||
def _require_local_decision_point(self) -> bool:
|
||||
if self._is_unproxied_loopback_request():
|
||||
return True
|
||||
|
||||
@@ -164,3 +164,13 @@ class TestMutationRequestSecurity:
|
||||
|
||||
handler.headers["X-Forwarded-For"] = "203.0.113.10"
|
||||
assert handler._hardware_access_bypass_allowed() is False
|
||||
|
||||
def test_wireless_passive_state_bypass_does_not_widen_control_bypass(self) -> None:
|
||||
handler = object.__new__(SeatDemoHandler)
|
||||
handler.client_address = ("127.0.0.1", 12345)
|
||||
handler.headers = Message()
|
||||
handler.auth = _auth(enabled=False)
|
||||
|
||||
assert handler._passive_state_access_bypass_allowed("wireless_mqtt") is True
|
||||
assert handler._passive_state_access_bypass_allowed("wired_can") is False
|
||||
assert handler._hardware_access_bypass_allowed() is False
|
||||
|
||||
@@ -135,6 +135,32 @@ class SeatWebApiTests(unittest.TestCase):
|
||||
unauthenticated.open(f"{self.base_url}/api/runtime", timeout=3)
|
||||
self.assertEqual(caught.exception.code, 401)
|
||||
|
||||
with self.assertRaises(urllib.error.HTTPError) as state_denied:
|
||||
unauthenticated.open(
|
||||
f"{self.base_url}/api/hardware/vehicle-state?"
|
||||
"conversation_id=conv-unauthenticated-state&"
|
||||
"control_transport=wireless_mqtt",
|
||||
timeout=3,
|
||||
)
|
||||
self.assertEqual(state_denied.exception.code, 401)
|
||||
|
||||
control_request = urllib.request.Request(
|
||||
f"{self.base_url}/api/can/commands",
|
||||
method="POST",
|
||||
headers={"Content-Type": "application/json"},
|
||||
data=json.dumps(
|
||||
{
|
||||
"commands": ["reading_light power on"],
|
||||
"conversation_id": "conv-unauthenticated-control",
|
||||
"execution_mode": "hardware",
|
||||
"control_transport": "wireless_mqtt",
|
||||
}
|
||||
).encode("utf-8"),
|
||||
)
|
||||
with self.assertRaises(urllib.error.HTTPError) as control_denied:
|
||||
unauthenticated.open(control_request, timeout=3)
|
||||
self.assertEqual(control_denied.exception.code, 401)
|
||||
|
||||
def test_n7_mode_scene_catalog_is_public_and_read_only(self) -> None:
|
||||
unauthenticated = urllib.request.build_opener()
|
||||
|
||||
@@ -667,18 +693,18 @@ operations:
|
||||
def test_static_console_and_runtime_endpoint(self) -> None:
|
||||
with self.opener.open(f"{self.base_url}/", timeout=3) as response:
|
||||
html = response.read().decode("utf-8")
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-131", timeout=3) as response:
|
||||
with self.opener.open(f"{self.base_url}/app.js?v=dual-seat-135", timeout=3) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/gesture-control.js?v=android-pad-9", timeout=3
|
||||
) as response:
|
||||
gesture_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.js?v=local-first-66", timeout=3
|
||||
f"{self.base_url}/console-v2.js?v=local-first-70", timeout=3
|
||||
) as response:
|
||||
console_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/console-v2.css?v=delivery-state-68", timeout=3
|
||||
f"{self.base_url}/console-v2.css?v=delivery-state-69", timeout=3
|
||||
) as response:
|
||||
console_styles = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -694,7 +720,7 @@ operations:
|
||||
) as response:
|
||||
speech_worklet = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/self-extension.js?v=gscu-tcp-43", timeout=3
|
||||
f"{self.base_url}/self-extension.js?v=gscu-tcp-44", timeout=3
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -702,7 +728,7 @@ operations:
|
||||
) as response:
|
||||
extension_styles = response.read().decode("utf-8").replace("\r\n", "\n")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-27", timeout=3
|
||||
f"{self.base_url}/assistant-chat.js?v=codex-assistant-32", timeout=3
|
||||
) as response:
|
||||
assistant_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
@@ -743,12 +769,12 @@ operations:
|
||||
self.assertIn("CMD 0x431 · FB 0x41A · CH1", html)
|
||||
self.assertIn("CMD 0x431 · FB 0x388 · CH1", html)
|
||||
self.assertNotIn("透明车体 · 实体设备", html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-131"', html)
|
||||
self.assertIn('src="/app.js?v=dual-seat-135"', html)
|
||||
self.assertIn('href="/styles.css?v=dual-seat-23"', html)
|
||||
self.assertIn('src="/gesture-control.js?v=android-pad-9"', html)
|
||||
self.assertNotIn('class="vehicle-system-hud"', html)
|
||||
self.assertIn('src="/console-v2.js?v=local-first-66"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-68"', html)
|
||||
self.assertIn('src="/console-v2.js?v=local-first-70"', html)
|
||||
self.assertIn('href="/console-v2.css?v=delivery-state-69"', html)
|
||||
self.assertIn('src="/speech-input.js?v=sensevoice-13"', html)
|
||||
self.assertIn('href="/speech-input.css?v=sensevoice-7"', html)
|
||||
self.assertIn('id="speechDictationButton"', html)
|
||||
@@ -756,7 +782,7 @@ operations:
|
||||
self.assertIn('<body class="console-v2">', html)
|
||||
self.assertIn('id="v2BootScreen"', html)
|
||||
self.assertIn('正在加载车机控制与实时状态', html)
|
||||
self.assertIn('src="/self-extension.js?v=gscu-tcp-43"', html)
|
||||
self.assertIn('src="/self-extension.js?v=gscu-tcp-44"', html)
|
||||
self.assertIn('href="/self-extension.css?v=gscu-tcp-43"', html)
|
||||
self.assertNotIn("self-calibration", html)
|
||||
self.assertNotIn("self-repair", html)
|
||||
@@ -764,8 +790,8 @@ operations:
|
||||
self.assertNotIn("滚动查看更多", html)
|
||||
self.assertIn('aria-label="发送消息"', html)
|
||||
self.assertIn('id="assistantTranscript"', html)
|
||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-26"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-27"', html)
|
||||
self.assertIn('href="/assistant-chat.css?v=codex-assistant-28"', html)
|
||||
self.assertIn('src="/assistant-chat.js?v=codex-assistant-32"', html)
|
||||
self.assertIn('href="/energy-monitor.css?v=energy-monitor-4"', html)
|
||||
self.assertIn('src="/energy-monitor.js?v=energy-monitor-4"', html)
|
||||
self.assertNotIn('"admin"', console_script)
|
||||
@@ -803,8 +829,12 @@ operations:
|
||||
'if (ui.activeModule === "self-extension" && role !== "user"',
|
||||
console_script,
|
||||
)
|
||||
self.assertIn('511工程-智能座舱', console_script)
|
||||
self.assertIn('网页控制链路', console_script)
|
||||
self.assertNotIn('511工程-智能座舱', console_script)
|
||||
self.assertNotIn('网页控制链路', console_script)
|
||||
self.assertIn('function buildModuleSwitcher(mainPanel, modeRail)', console_script)
|
||||
self.assertIn('if (modeRailBar) modeRailBar.prepend(switcher)', console_script)
|
||||
self.assertNotIn('class="v2-sync-sidebar-heading"', console_script)
|
||||
self.assertIn('function appendDispatchedControls(message, result = {})', assistant_script)
|
||||
self.assertIn('["self-orchestration", "self-extension"]', console_script)
|
||||
self.assertNotIn('label: "自诊断"', console_script)
|
||||
self.assertNotIn('runtimeControls.id = "v2RuntimeControls"', console_script)
|
||||
@@ -1043,14 +1073,14 @@ operations:
|
||||
self.assertIn("CAN 指令已提交,等待车辆状态确认", console_script)
|
||||
self.assertIn('canvas.addEventListener("webglcontextlost"', app_script)
|
||||
self.assertIn('"cognitive-agent:module-change"', app_script)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v282"', service_worker)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-68', service_worker)
|
||||
self.assertIn('/console-v2.js?v=local-first-66', service_worker)
|
||||
self.assertIn('/assistant-chat.css?v=codex-assistant-26', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-27', service_worker)
|
||||
self.assertIn('const CACHE_VERSION = "cognitive-agent-pad-v290"', service_worker)
|
||||
self.assertIn('/console-v2.css?v=delivery-state-69', service_worker)
|
||||
self.assertIn('/console-v2.js?v=local-first-70', service_worker)
|
||||
self.assertIn('/assistant-chat.css?v=codex-assistant-28', service_worker)
|
||||
self.assertIn('/assistant-chat.js?v=codex-assistant-32', service_worker)
|
||||
self.assertIn('/energy-monitor.css?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('/energy-monitor.js?v=energy-monitor-4', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-131"', service_worker)
|
||||
self.assertIn('"/app.js?v=dual-seat-135"', service_worker)
|
||||
self.assertIn('"/speech-input.js?v=sensevoice-13"', service_worker)
|
||||
self.assertIn('"/speech-input.css?v=sensevoice-7"', service_worker)
|
||||
self.assertIn('"/speech-audio-worklet.js?v=sensevoice-5"', service_worker)
|
||||
@@ -1507,7 +1537,7 @@ operations:
|
||||
) as response:
|
||||
extension_script = response.read().decode("utf-8")
|
||||
with self.opener.open(
|
||||
f"{self.base_url}/app.js?v=dual-seat-131", timeout=3
|
||||
f"{self.base_url}/app.js?v=dual-seat-135", timeout=3
|
||||
) as response:
|
||||
app_script = response.read().decode("utf-8")
|
||||
|
||||
@@ -3100,6 +3130,53 @@ operations:
|
||||
self.assertEqual(payload["source"], "wireless_mqtt_ch1")
|
||||
self.assertEqual(payload["capture_sequence"], 17)
|
||||
|
||||
def test_wireless_state_http_bypasses_only_the_physical_bench_lease(self) -> None:
|
||||
cases = (
|
||||
("seat-state", "synchronize_hardware_state", "conv-http-mqtt-seat-state"),
|
||||
("vehicle-state", "synchronize_vehicle_state", "conv-http-mqtt-vehicle-state"),
|
||||
)
|
||||
for endpoint, method_name, conversation_id in cases:
|
||||
with self.subTest(endpoint=endpoint):
|
||||
with patch.object(
|
||||
self.service,
|
||||
method_name,
|
||||
return_value={"status": "succeeded", "source": "wireless_mqtt"},
|
||||
) as synchronize:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/hardware/{endpoint}?"
|
||||
f"conversation_id={conversation_id}&vehicle_id=denza_n7&"
|
||||
"control_transport=wireless_mqtt&"
|
||||
"hardware_client_id=hwc-e2e-state-reader",
|
||||
headers={"X-Forwarded-For": "203.0.113.25"},
|
||||
)
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
synchronize.assert_called_once_with(
|
||||
conversation_id,
|
||||
"denza_n7",
|
||||
control_transport="wireless_mqtt",
|
||||
hardware_client_id="hwc-e2e-state-reader",
|
||||
hardware_access_bypass=True,
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
self.service,
|
||||
"synchronize_vehicle_state",
|
||||
return_value={"status": "succeeded", "source": "physical_can_ch1"},
|
||||
) as synchronize_wired:
|
||||
request = urllib.request.Request(
|
||||
f"{self.base_url}/api/hardware/vehicle-state?"
|
||||
"conversation_id=conv-http-wired-state&vehicle_id=denza_n7&"
|
||||
"control_transport=wired_can&"
|
||||
"hardware_client_id=hwc-e2e-state-reader",
|
||||
headers={"X-Forwarded-For": "203.0.113.25"},
|
||||
)
|
||||
with self.opener.open(request, timeout=3) as response:
|
||||
self.assertEqual(response.status, 200)
|
||||
|
||||
self.assertFalse(synchronize_wired.call_args.kwargs["hardware_access_bypass"])
|
||||
|
||||
def test_gpt_http_failure_falls_back_without_returning_502(self) -> None:
|
||||
original_api_key = self.service.openai.api_key
|
||||
self.service.openai.api_key = "test-key"
|
||||
|
||||
@@ -309,6 +309,30 @@ test("assistant bridges request start and validated local controls to the consol
|
||||
value: "0x1DE · 00 00 00 00 17 00 00 00",
|
||||
}],
|
||||
},
|
||||
{
|
||||
operation: {
|
||||
id: "reading-light-1",
|
||||
capability: "interior_light.set_reading_power",
|
||||
target: { instance: "all" },
|
||||
parameters: { state: "off" },
|
||||
parse_source: "local_rule",
|
||||
},
|
||||
channel: "CAN1",
|
||||
actual_command: "light reading all off",
|
||||
frames: [],
|
||||
},
|
||||
{
|
||||
operation: {
|
||||
id: "roof-shade-1",
|
||||
capability: "roof.set_shade_motion",
|
||||
target: { instance: "shade" },
|
||||
parameters: { motion: "close" },
|
||||
parse_source: "local_rule",
|
||||
},
|
||||
channel: "CAN1",
|
||||
actual_command: "shade close",
|
||||
frames: [],
|
||||
},
|
||||
],
|
||||
trace_events: [{ stage: "compile", duration_ms: 2 }],
|
||||
};
|
||||
@@ -338,6 +362,25 @@ test("assistant bridges request start and validated local controls to the consol
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(validated)), [payload]);
|
||||
assert.equal(harness.requestCalls.length, 1);
|
||||
assert.equal(JSON.parse(harness.requestCalls[0].options.body).execution_mode, "dry_run");
|
||||
|
||||
const assistantArticle = harness.messages.children[1];
|
||||
const cards = assistantArticle.querySelectorAll(".assistant-dispatched-controls");
|
||||
assert.equal(cards.length, 1);
|
||||
assert.equal(cards[0].open, true);
|
||||
assert.equal(
|
||||
cards[0].querySelector(".assistant-dispatched-controls-count").textContent,
|
||||
"3",
|
||||
);
|
||||
const rows = cards[0].querySelector(".assistant-dispatched-controls-list").children;
|
||||
assert.deepEqual(
|
||||
rows.map((row) => row.querySelector(".assistant-dispatched-control-label").textContent),
|
||||
["空调温度", "阅读灯", "遮阳帘"],
|
||||
);
|
||||
assert.deepEqual(
|
||||
rows.map((row) => row.querySelector(".assistant-dispatched-control-value").textContent),
|
||||
["23°C", "关闭", "关闭"],
|
||||
);
|
||||
assert.match(rows[0].title, /实际控制指令:ac temp driver 23/);
|
||||
});
|
||||
|
||||
test("unsupported-control suggestions render once and click through as a new command", async () => {
|
||||
|
||||
@@ -13,15 +13,15 @@ def test_console_contains_ai_assistant_surface():
|
||||
assert 'aria-label="AI助手对话"' in html
|
||||
assert 'placeholder="向 AI助手提问或交代工作"' in html
|
||||
assert 'aria-label="发送消息"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-26"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-27"' in html
|
||||
assert 'href="/assistant-chat.css?v=codex-assistant-28"' in html
|
||||
assert 'src="/assistant-chat.js?v=codex-assistant-32"' in html
|
||||
|
||||
|
||||
def test_codex_assistant_assets_are_versioned_together_in_the_service_worker():
|
||||
service_worker = (ROOT / "apps/web-console/service-worker.js").read_text(encoding="utf-8")
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v282"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-26"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-27"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v290"' in service_worker
|
||||
assert '"/assistant-chat.css?v=codex-assistant-28"' in service_worker
|
||||
assert '"/assistant-chat.js?v=codex-assistant-32"' in service_worker
|
||||
|
||||
|
||||
def test_codex_frontend_uses_one_streaming_endpoint_and_agent_work_events():
|
||||
@@ -73,6 +73,31 @@ def test_unsupported_control_suggestions_are_clickable_accessible_and_deduplicat
|
||||
assert ".assistant-control-suggestion.is-sending" in css
|
||||
|
||||
|
||||
def test_validated_controls_are_mirrored_into_a_compact_assistant_card():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
css = (ROOT / "apps/web-console/assistant-chat.css").read_text(encoding="utf-8")
|
||||
|
||||
assert "function appendDispatchedControls(message, result = {})" in script
|
||||
assert "window.CognitiveAgentConsoleV2?.getParsedCanCommands?.(result)" in script
|
||||
assert "fallbackAssistantControlCommands(result)" in script
|
||||
assert 'card.className = "assistant-dispatched-controls"' in script
|
||||
assert 'heading.textContent = "下发控制指令"' in script
|
||||
assert 'count.className = "assistant-dispatched-controls-count"' in script
|
||||
assert 'list.className = "assistant-dispatched-controls-list"' in script
|
||||
assert "value.textContent = assistantControlValue(item)" in script
|
||||
assert "语义控制指令:" in script
|
||||
assert "实际控制指令:" in script
|
||||
assert "appendDispatchedControls(message, event.payload)" in script
|
||||
assert "function announceModeExecution(" in script
|
||||
assert "announceModeExecution," in script
|
||||
assert '"ambient_light.set_illumination": "氛围灯颜色与亮度"' in script
|
||||
assert '.replaceAll("_", "-")' in script
|
||||
assert ".assistant-dispatched-controls > summary" in css
|
||||
assert ".assistant-dispatched-controls-list > li" in css
|
||||
assert ".assistant-dispatched-control-value" in css
|
||||
assert ".assistant-dispatched-controls[open]" in css
|
||||
|
||||
|
||||
def test_codex_request_keeps_trusted_vehicle_context_without_legacy_model_routing():
|
||||
script = (ROOT / "apps/web-console/assistant-chat.js").read_text(encoding="utf-8")
|
||||
request_body = script.split("function requestBody(message)", 1)[1].split(
|
||||
|
||||
@@ -35,10 +35,10 @@ def test_energy_monitor_assets_are_versioned_and_cached() -> None:
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-27") < html.index(
|
||||
assert html.index("/assistant-chat.js?v=codex-assistant-32") < html.index(
|
||||
"/energy-monitor.js?v=energy-monitor-4"
|
||||
)
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v282"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v290"' in service_worker
|
||||
|
||||
|
||||
def test_energy_monitor_uses_the_fifth_shared_dock_icon_and_starts_closed() -> None:
|
||||
|
||||
@@ -41,13 +41,13 @@ def test_theme_asset_versions_are_consistent_with_the_pwa_shell() -> None:
|
||||
|
||||
for asset in (
|
||||
"/styles.css?v=dual-seat-23",
|
||||
"/console-v2.css?v=delivery-state-68",
|
||||
"/console-v2.css?v=delivery-state-69",
|
||||
"/self-extension.css?v=gscu-tcp-43",
|
||||
"/speech-input.css?v=sensevoice-7",
|
||||
):
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v282"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v290"' in service_worker
|
||||
|
||||
|
||||
def test_vehicle_twin_icons_and_nested_overlays_use_semantic_tokens() -> None:
|
||||
|
||||
@@ -259,6 +259,39 @@ class FrontendVehicleStateSyncContractTests(unittest.TestCase):
|
||||
)
|
||||
self.assertNotIn("passenger: mergedActuatorState.driver", render_source)
|
||||
|
||||
def test_state_polling_surfaces_structured_403_reason(self) -> None:
|
||||
passenger_source = function_source(
|
||||
self.app_source,
|
||||
"async function synchronizePassengerSeatState() {",
|
||||
"\nasync function synchronizeDriverSeatState()",
|
||||
)
|
||||
driver_source = function_source(
|
||||
self.app_source,
|
||||
"async function synchronizeDriverSeatState() {",
|
||||
"\nasync function synchronizePhysicalSeatState()",
|
||||
)
|
||||
live_status_source = function_source(
|
||||
self.app_source,
|
||||
"function renderCabinLiveStatus(",
|
||||
"\nfunction refreshIcons",
|
||||
)
|
||||
telemetry_source = function_source(
|
||||
self.app_source,
|
||||
"function recordVehicleStateTelemetry(payload) {",
|
||||
"\nfunction applyAuthoritativeVehicleState",
|
||||
)
|
||||
|
||||
self.assertIn("throw await apiResponseError(response)", passenger_source)
|
||||
self.assertIn("throw await apiResponseError(response)", driver_source)
|
||||
self.assertIn("errorStatus: Number(error.status) || null", driver_source)
|
||||
self.assertIn("errorReason: error.serverMessage", driver_source)
|
||||
self.assertIn("snapshot?.errorStatus === 403", live_status_source)
|
||||
self.assertIn("`${transportLabel} · 403:${reason}`", live_status_source)
|
||||
self.assertIn('telemetryState.textContent = "403 拒绝"', live_status_source)
|
||||
self.assertIn('setSummarySystem("403 · 状态读取被拒绝", "warning")', live_status_source)
|
||||
self.assertIn("errorStatus: null", telemetry_source)
|
||||
self.assertIn('errorReason: ""', telemetry_source)
|
||||
|
||||
def test_persistent_linux_monitor_uses_backend_selected_fast_polling(self) -> None:
|
||||
interval_source = function_source(
|
||||
self.app_source,
|
||||
|
||||
@@ -250,7 +250,32 @@ def test_top_mode_action_formatter_uses_device_target_action_and_parameter_value
|
||||
}
|
||||
|
||||
|
||||
def test_mode_cards_open_assistant_and_submit_enter_mode_command() -> None:
|
||||
def test_mode_chip_scroller_keeps_clicks_and_only_captures_real_drags() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
start = app.index("function installModeChipScroller")
|
||||
end = app.index("\n\nfunction ensureModeActionPopover", start)
|
||||
snippet = app[start:end]
|
||||
|
||||
threshold = snippet.index('if (!dragged && Math.abs(distance) < 5) return;')
|
||||
capture = snippet.index("group.setPointerCapture(pointerId);")
|
||||
assert capture > threshold
|
||||
assert "let pointerCaptured = false;" in snippet
|
||||
assert "if (pointerCaptured) group.releasePointerCapture?.(pointerId);" in snippet
|
||||
assert 'event.stopImmediatePropagation();' in snippet
|
||||
|
||||
|
||||
def test_top_mode_triangle_is_an_explicit_execution_target() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
vue_ui = (WEB / "vue-ui.js").read_text(encoding="utf-8")
|
||||
handlers = app[app.index('document.addEventListener("DOMContentLoaded"') :]
|
||||
|
||||
assert '"data-mode-run": "true"' in vue_ui
|
||||
assert 'runIcon.dataset.modeRun = "true";' in app
|
||||
assert 'const runControl = target?.closest("[data-mode-run]");' in handlers
|
||||
assert '(runControl || target)?.closest("button[data-mode-command]")' in handlers
|
||||
|
||||
|
||||
def test_saved_mode_cards_open_assistant_and_execute_the_saved_command_flow() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
start = app.index("function applyModeCommand")
|
||||
end = app.index("\n\nfunction applyRuntimeTargetPolicy", start)
|
||||
@@ -258,12 +283,6 @@ def test_mode_cards_open_assistant_and_submit_enter_mode_command() -> None:
|
||||
script = "const snippet = " + json.dumps(snippet) + ";\n" + r"""
|
||||
const vm = require("node:vm");
|
||||
const calls = [];
|
||||
const input = {
|
||||
value: "",
|
||||
dataset: { speechStage: "final", speechUtteranceId: "old", speechValue: "old" },
|
||||
dispatchEvent(event) { calls.push(["input", event.type, event.bubbles, this.value]); },
|
||||
};
|
||||
const form = { requestSubmit() { calls.push(["submit", input.value]); } };
|
||||
const runButton = { disabled: false };
|
||||
const savedMode = {
|
||||
id: "reading-user",
|
||||
@@ -283,11 +302,14 @@ const context = {
|
||||
},
|
||||
modeProfiles() { return [savedMode]; },
|
||||
setModeRailSelection(mode) { calls.push(["select", mode?.id]); },
|
||||
setModeExecutionState(modeId, status) { calls.push(["state", modeId, status]); },
|
||||
closeModeGallery(options) { calls.push(["close", options.restoreFocus]); },
|
||||
showToast(message) { calls.push(["toast", message]); },
|
||||
executeSavedModeProfile(mode, options) {
|
||||
calls.push(["execute", mode.id, options.command]);
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
$(selector) {
|
||||
return { "#utterance": input, "#requestForm": form, "#runButton": runButton }[selector];
|
||||
return { "#runButton": runButton }[selector];
|
||||
},
|
||||
};
|
||||
vm.createContext(context);
|
||||
@@ -295,8 +317,6 @@ vm.runInContext(snippet + '\nthis.result = applyModeCommand(" 进入阅读模式
|
||||
console.log(JSON.stringify({
|
||||
result: context.result,
|
||||
calls,
|
||||
value: input.value,
|
||||
speechDataset: input.dataset,
|
||||
pendingModeProfileId: context.state.pendingModeProfileId,
|
||||
}));
|
||||
"""
|
||||
@@ -314,14 +334,71 @@ console.log(JSON.stringify({
|
||||
["select", "reading-user"],
|
||||
["close", False],
|
||||
["open"],
|
||||
["input", "input", True, "进入阅读模式"],
|
||||
["state", "reading-user", "running"],
|
||||
["submit", "进入阅读模式"],
|
||||
["execute", "reading-user", "进入阅读模式"],
|
||||
]
|
||||
assert result["value"] == "进入阅读模式"
|
||||
assert result["speechDataset"] == {}
|
||||
assert result["pendingModeProfileId"] == "reading-user"
|
||||
assert "executeSavedModeProfile" not in snippet
|
||||
assert result["pendingModeProfileId"] is None
|
||||
assert "executeSavedModeProfile" in snippet
|
||||
|
||||
|
||||
def test_mode_card_without_saved_flow_falls_back_to_assistant_submission() -> None:
|
||||
app = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
start = app.index("function applyModeCommand")
|
||||
end = app.index("\n\nfunction applyRuntimeTargetPolicy", start)
|
||||
snippet = app[start:end]
|
||||
script = "const snippet = " + json.dumps(snippet) + ";\n" + r"""
|
||||
const vm = require("node:vm");
|
||||
const calls = [];
|
||||
const input = {
|
||||
value: "",
|
||||
dataset: { speechStage: "final", speechUtteranceId: "old", speechValue: "old" },
|
||||
dispatchEvent(event) { calls.push(["input", event.type, event.bubbles, this.value]); },
|
||||
};
|
||||
const form = { requestSubmit() { calls.push(["submit", input.value]); } };
|
||||
const runButton = { disabled: false };
|
||||
const unsavedMode = { id: "future-mode", name: "未来模式" };
|
||||
const context = {
|
||||
state: { pendingModeProfileId: null },
|
||||
Event: class Event {
|
||||
constructor(type, options = {}) { this.type = type; this.bubbles = options.bubbles; }
|
||||
},
|
||||
window: {
|
||||
CognitiveAgentAssistant: {
|
||||
isPending() { return false; },
|
||||
open() { calls.push(["open"]); },
|
||||
},
|
||||
},
|
||||
modeProfiles() { return [unsavedMode]; },
|
||||
setModeRailSelection(mode) { calls.push(["select", mode?.id]); },
|
||||
setModeExecutionState(modeId, status) { calls.push(["state", modeId, status]); },
|
||||
closeModeGallery(options) { calls.push(["close", options.restoreFocus]); },
|
||||
showToast(message) { calls.push(["toast", message]); },
|
||||
$(selector) {
|
||||
return { "#utterance": input, "#requestForm": form, "#runButton": runButton }[selector];
|
||||
},
|
||||
};
|
||||
vm.createContext(context);
|
||||
vm.runInContext(snippet + '\nthis.result = applyModeCommand(" 进入未来模式 ", "future-mode");', context);
|
||||
console.log(JSON.stringify({ result: context.result, calls, value: input.value }));
|
||||
"""
|
||||
completed = subprocess.run(
|
||||
[node_runtime(), "-e", script],
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
result = json.loads(completed.stdout)
|
||||
|
||||
assert result["result"] is True
|
||||
assert result["calls"] == [
|
||||
["select", "future-mode"],
|
||||
["close", False],
|
||||
["open"],
|
||||
["input", "input", True, "进入未来模式"],
|
||||
["state", "future-mode", "running"],
|
||||
["submit", "进入未来模式"],
|
||||
]
|
||||
assert result["value"] == "进入未来模式"
|
||||
|
||||
|
||||
def test_mode_cards_do_not_submit_while_assistant_is_busy() -> None:
|
||||
|
||||
@@ -741,8 +741,9 @@ class MqttCanTransportTests(unittest.TestCase):
|
||||
by_id["0x41A"]["domain_state"]["roof"]["shade_position_pct"],
|
||||
92,
|
||||
)
|
||||
self.assertIsNone(
|
||||
by_id["0x42E"]["domain_state"]["ambient_light"]["front_color_index"]
|
||||
self.assertEqual(
|
||||
by_id["0x42E"]["domain_state"]["ambient_light"]["front_color_index"],
|
||||
100,
|
||||
)
|
||||
self.assertEqual(
|
||||
by_id["0x3FF"]["domain_state"]["interior_light"]["ambient_illuminance_lux"],
|
||||
|
||||
@@ -100,9 +100,23 @@ class NativeCanFeedbackTests(unittest.TestCase):
|
||||
self.assertFalse(feedback_matches("indoor_light:on", feedback))
|
||||
self.assertTrue(feedback_matches("ambient_power:off", feedback))
|
||||
|
||||
def test_can1_matrix_rejects_reserved_ambient_color_values(self) -> None:
|
||||
def test_n7_profile_accepts_full_seven_bit_ambient_color_index(self) -> None:
|
||||
decoded = decode_feedback(
|
||||
self.frame(0x42E, "11 64 32 03 90 02 00 C6")
|
||||
self.frame(0x42E, "11 C1 20 03 D0 02 00 C6")
|
||||
)["domain_state"]["ambient_light"]
|
||||
|
||||
self.assertEqual(decoded["front_color_index"], 65)
|
||||
self.assertEqual(decoded["rear_color_index"], 65)
|
||||
self.assertTrue(
|
||||
feedback_matches(
|
||||
"ambient_color:65",
|
||||
self.frame(0x42E, "11 C1 20 03 D0 02 00 C6"),
|
||||
)
|
||||
)
|
||||
|
||||
def test_can1_matrix_rejects_zero_ambient_color_index(self) -> None:
|
||||
decoded = decode_feedback(
|
||||
self.frame(0x42E, "11 00 00 03 D0 02 00 C6")
|
||||
)["domain_state"]["ambient_light"]
|
||||
|
||||
self.assertIsNone(decoded["front_color_index"])
|
||||
|
||||
@@ -60,7 +60,7 @@ def test_unified_console_exposes_device_id_based_nfc_authorization() -> None:
|
||||
assert '.v2-nfc-command-list' in styles
|
||||
assert '.v2-nfc-command-option' in styles
|
||||
assert '.v2-nfc-select-all' in styles
|
||||
assert 'self-extension.js?v=gscu-tcp-43' in html
|
||||
assert 'self-extension.js?v=gscu-tcp-44' in html
|
||||
assert 'self-extension.css?v=gscu-tcp-43' in html
|
||||
|
||||
|
||||
@@ -140,5 +140,5 @@ def test_main_parsed_control_panel_renders_validated_infrared_operations() -> No
|
||||
assert "下发控制指令" in console
|
||||
assert "已解析控制指令" not in console
|
||||
assert 'li[data-can-channel="IR"]' in styles
|
||||
assert "console-v2.js?v=local-first-66" in html
|
||||
assert "console-v2.js?v=local-first-70" in html
|
||||
assert 'speech_semantic: "语音语义"' in console
|
||||
|
||||
@@ -30,7 +30,7 @@ def test_frontend_only_exposes_user_orchestration_and_extension() -> None:
|
||||
assert not (web_root / removed).exists()
|
||||
assert removed not in html
|
||||
|
||||
assert "511工程-智能座舱" in console
|
||||
assert "511工程-智能座舱" not in console
|
||||
assert '["self-orchestration", "self-extension"]' in console
|
||||
assert 'id="vehicleExtensionSync"' in console
|
||||
assert 'getRole: () => "user"' in console
|
||||
@@ -174,6 +174,20 @@ def test_vehicle_device_status_cards_prioritize_seats_and_expose_manual_controls
|
||||
assert ".v2-device-control-apply" in styles
|
||||
|
||||
|
||||
def test_ambient_status_uses_its_own_power_feedback_not_combined_light_badge() -> None:
|
||||
console = source("console-v2.js")
|
||||
ambient_start = console.index('id: "ambient", name: "氛围灯"')
|
||||
ambient_end = console.index('id: "headlight", name: "前大灯"', ambient_start)
|
||||
ambient = console[ambient_start:ambient_end]
|
||||
|
||||
assert 'function ambientPowerState()' in console
|
||||
assert 'dataset.ambientPower' in console
|
||||
assert 'const powerState = ambientPowerState();' in ambient
|
||||
assert 'powerState === "on" ? "开启"' in ambient
|
||||
assert 'ambientPowerState() === "off" ? "off" : "on"' in ambient
|
||||
assert 'readText("#ambientPower")' not in ambient
|
||||
|
||||
|
||||
def test_passenger_can0_status_ui_uses_only_seat_ota_four_axis_contract() -> None:
|
||||
app = source("app.js")
|
||||
console = source("console-v2.js")
|
||||
@@ -207,7 +221,8 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
|
||||
assert "getParsedCanCommands: (result) => parsedCanCommands(result)" in console
|
||||
assert "const configurations = result.function_configs || {};" in console
|
||||
assert "if (!configurationEntries.length && result.function_config)" in console
|
||||
assert 'deliveryLabel: submitted ? "下发" : "未下发"' in console
|
||||
assert "const safetyBlocked = executionFailed && !submitted" in console
|
||||
assert 'safetyBlocked\n ? "安全阻止"' in console
|
||||
assert 'data-delivery-state="${escapeHtml(item.deliveryState)}"' in console
|
||||
assert 'const command = (category, text, channel = "CAN1", frame = "")' in console
|
||||
assert 'data-can-channel="${escapeHtml(item.channel)}"' in console
|
||||
@@ -247,6 +262,9 @@ def test_left_sidebar_exposes_collapsed_parsed_can_command_panel() -> None:
|
||||
"light reading all ${onOff}",
|
||||
"light ambient color ${color}",
|
||||
"light ambient brightness ${level}",
|
||||
"Ambient_RGB_Control_32B_2B(color=${color}, brightness=${brightness}%)",
|
||||
"Ambient_Mode_Control_496_04(mode=${mode})",
|
||||
"Climate_Control_064_04(${target}=${temperature}°C)",
|
||||
"shade position ${position}",
|
||||
"seat passenger ${axis[1]} position ${position}",
|
||||
"seat passenger dynamic ${dynamic}",
|
||||
@@ -705,6 +723,42 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
|
||||
}
|
||||
}
|
||||
}
|
||||
blocked_shade = {
|
||||
"function_configs": {
|
||||
"native_roof": {
|
||||
"spec": {
|
||||
"operations": [
|
||||
{
|
||||
"id": "shade-position",
|
||||
"capability": "roof.set_shade_position",
|
||||
"target": {"zone": "vehicle"},
|
||||
"parameters": {"position_pct": 30},
|
||||
"parse_source": "local_rule",
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"execution_mode": "hardware",
|
||||
"control_transport": "wireless_mqtt",
|
||||
"execution_report": {
|
||||
"spec": {
|
||||
"results": [
|
||||
{
|
||||
"ref": "shade-position",
|
||||
"status": "failed",
|
||||
"message": (
|
||||
"no fresh valid 0x41A shade position; "
|
||||
"refusing blind motion"
|
||||
),
|
||||
"telemetry": {
|
||||
"verification_status": "failed_or_blocked"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
samples = [
|
||||
{**base, "execution_mode": "dry_run", "control_transport": "wired_can"},
|
||||
{
|
||||
@@ -744,6 +798,7 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
|
||||
}
|
||||
},
|
||||
},
|
||||
blocked_shade,
|
||||
]
|
||||
script = (
|
||||
f"{helpers}\n"
|
||||
@@ -761,19 +816,35 @@ def test_parsed_command_delivery_is_independent_of_transport_and_target() -> Non
|
||||
)
|
||||
|
||||
parsed = json.loads(completed.stdout)
|
||||
assert [len(commands) for commands in parsed] == [1, 1, 1, 1]
|
||||
assert [commands[0]["command"] for commands in parsed] == ["ac on"] * 4
|
||||
assert [len(commands) for commands in parsed] == [1, 1, 1, 1, 1]
|
||||
assert [commands[0]["command"] for commands in parsed] == [
|
||||
"ac on",
|
||||
"ac on",
|
||||
"ac on",
|
||||
"ac on",
|
||||
"shade position 30",
|
||||
]
|
||||
assert [commands[0]["deliveryLabel"] for commands in parsed] == [
|
||||
"未下发",
|
||||
"未下发",
|
||||
"下发",
|
||||
"下发",
|
||||
"安全阻止",
|
||||
]
|
||||
assert [commands[0]["deliveryState"] for commands in parsed] == [
|
||||
"not_submitted",
|
||||
"not_submitted",
|
||||
"submitted",
|
||||
"submitted",
|
||||
"blocked",
|
||||
]
|
||||
assert "0x41A 实时位置" in parsed[-1][0]["deliveryDetail"]
|
||||
assert [commands[0]["deliveryTransport"] for commands in parsed] == [
|
||||
"有线 CAN",
|
||||
"无线 CAN",
|
||||
"有线 CAN",
|
||||
"无线 CAN",
|
||||
"无线 CAN",
|
||||
]
|
||||
|
||||
|
||||
@@ -803,9 +874,11 @@ def test_mode_editor_renders_readable_commands_with_expandable_mqtt_sequences()
|
||||
assert "if (isDispatchTask(task))" in console
|
||||
assert "下发控制指令" in console
|
||||
assert "已解析控制指令" not in console
|
||||
assert "function executeSavedModeProfile(mode)" in app
|
||||
assert 'async function executeSavedModeProfile(mode, { command = "" } = {})' in app
|
||||
assert "/api/mode-profiles/${encodeURIComponent(mode.id)}/execute" in app
|
||||
assert '$("#utterance").value = mode.name || mode.id;' in app
|
||||
assert "assistant?.announceModeExecution?.(mode, payload" in app
|
||||
assert "void executeSavedModeProfile(mode, { command: modeCommand });" in app
|
||||
assert "announceModeExecution" in source("assistant-chat.js")
|
||||
assert 'const savedModeExecution = result.orchestration_route === "saved_mode_command_flow";' in app
|
||||
assert "`${result.mode_profile.name}执行完成`" in app
|
||||
|
||||
@@ -1021,6 +1094,14 @@ def test_interactive_console_uses_one_top_right_icon_dock_for_all_five_panels()
|
||||
assert console.index('id="v2DevicePanel"') < console.index('id="v2ExtensionPanel"')
|
||||
assert 'class="v2-side-status-stack"' in console
|
||||
assert 'class="v2-panel-control-dock"' in console
|
||||
assert "function buildModuleSwitcher(mainPanel, modeRail)" in console
|
||||
assert 'const modeRailBar = $(".mode-rail-bar", modeRail);' in console
|
||||
assert "if (modeRailBar) modeRailBar.prepend(switcher);" in console
|
||||
assert 'const homepageHeader = moduleSwitcher.closest(".v2-mode-rail") || moduleSwitcher;' in extension
|
||||
assert 'homepageHeader.insertAdjacentElement("afterend", root);' in extension
|
||||
assert 'class="v2-unified-brand"' not in console
|
||||
assert "网页控制链路" not in console
|
||||
assert '<div class="v2-sync-sidebar-heading">' not in console
|
||||
assert 'panelDock.append(sidebarToggle)' in console
|
||||
assert 'panelDock.append(deviceToggle, extensionToggle)' in console
|
||||
assert 'class="v2-side-panel-dock"' not in console
|
||||
@@ -1046,12 +1127,22 @@ def test_interactive_console_uses_one_top_right_icon_dock_for_all_five_panels()
|
||||
assert '.v2-panel-control-button[aria-pressed="true"]' in styles
|
||||
assert "background: var(--primary);" in styles
|
||||
assert "width: 38px !important;" in styles
|
||||
assert "Unified homepage header: panel controls and function modes share one row." in styles
|
||||
assert "grid-template-columns: auto auto minmax(0, 1fr) auto;" in styles
|
||||
final_styles = styles.split("/* AI assistant light-blue final override", 1)[1]
|
||||
assert 'data-open-panels="none"' in final_styles
|
||||
assert "grid-template-columns: minmax(0, 1fr);" in final_styles
|
||||
assert ".v2-unified-right-rail[hidden]" in final_styles
|
||||
|
||||
|
||||
def test_runtime_hardware_coverage_defines_the_linked_readiness_state() -> None:
|
||||
app = source("app.js")
|
||||
|
||||
definition = "const linkedHardwareReady = seatHardwareReady && vehicleHardwareReady;"
|
||||
assert definition in app
|
||||
assert app.index(definition) < app.index(": linkedHardwareReady")
|
||||
|
||||
|
||||
def test_left_sync_rail_can_collapse_and_log_aligns_with_lower_workspace() -> None:
|
||||
console = source("console-v2.js")
|
||||
styles = source("console-v2.css")
|
||||
@@ -1210,7 +1301,11 @@ def test_compact_mode_strip_supports_wheel_drag_and_keyboard_navigation() -> Non
|
||||
|
||||
assert 'group.addEventListener("pointerdown"' in scroller
|
||||
assert 'event.pointerType !== "mouse"' in scroller
|
||||
assert "group.setPointerCapture?.(pointerId)" in scroller
|
||||
assert "group.setPointerCapture(pointerId)" in scroller
|
||||
assert scroller.index("group.setPointerCapture(pointerId)") > scroller.index(
|
||||
"Math.abs(distance) < 5"
|
||||
)
|
||||
assert "let pointerCaptured = false;" in scroller
|
||||
assert 'group.addEventListener("pointermove"' in scroller
|
||||
assert "Math.abs(distance) < 5" in scroller
|
||||
assert 'group.classList.add("is-dragging")' in scroller
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WEB = ROOT / "apps" / "web-console"
|
||||
|
||||
|
||||
def source(name: str) -> str:
|
||||
return (WEB / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_spatial_ui_contract_is_loaded_last_and_cached() -> None:
|
||||
html = source("index.html")
|
||||
service_worker = source("service-worker.js")
|
||||
asset = "/ui-consistency.css?v=spatial-ui-4"
|
||||
|
||||
assert asset in html
|
||||
assert f'"{asset}"' in service_worker
|
||||
assert html.index("/energy-monitor.css") < html.index(asset)
|
||||
|
||||
|
||||
def test_same_kind_surfaces_share_geometry_tokens() -> None:
|
||||
styles = source("ui-consistency.css")
|
||||
|
||||
for token in (
|
||||
"--ui-control-height-compact:",
|
||||
"--ui-control-height:",
|
||||
"--ui-radius-control:",
|
||||
"--ui-radius-item:",
|
||||
"--ui-radius-section:",
|
||||
"--ui-radius-panel:",
|
||||
"--ui-radius-modal:",
|
||||
"--ui-shadow-item:",
|
||||
"--ui-shadow-panel:",
|
||||
"--ui-shadow-modal:",
|
||||
):
|
||||
assert token in styles
|
||||
|
||||
for selector in (
|
||||
".v2-operation-shell",
|
||||
".joint-twin-shell > .twin-band",
|
||||
"#v2DevicePanel",
|
||||
"#v2ExtensionPanel .v2-extension-sync-mount",
|
||||
".mode-gallery",
|
||||
".mode-editor",
|
||||
".energy-monitor-page",
|
||||
".v2-synced-device",
|
||||
".v2-extension-gateway",
|
||||
".mode-gallery-card",
|
||||
".energy-kpi",
|
||||
):
|
||||
assert selector in styles
|
||||
|
||||
|
||||
def test_same_kind_controls_share_sizes_and_focus_treatment() -> None:
|
||||
styles = source("ui-consistency.css")
|
||||
|
||||
for selector in (
|
||||
"#v2PanelControlDock > .v2-panel-control-button",
|
||||
".v2-mode-rail .mode-rail-actions > button",
|
||||
".v2-device-adjust-toggle",
|
||||
".v2-extension-nfc-launch",
|
||||
".energy-trend-tabs button",
|
||||
".mode-gallery-close",
|
||||
".mode-editor-header > button",
|
||||
"#energyMonitorClose",
|
||||
):
|
||||
assert selector in styles
|
||||
|
||||
assert "box-shadow: var(--ui-focus-ring)" in styles
|
||||
assert "outline: none" in styles
|
||||
|
||||
|
||||
def test_compact_sidebar_overrides_cannot_break_shared_geometry() -> None:
|
||||
styles = source("ui-consistency.css")
|
||||
|
||||
assert ".v2-sync-execution-field," in styles
|
||||
assert ".v2-can-command-panel" in styles
|
||||
assert "border-radius: var(--ui-radius-item) !important" in styles
|
||||
assert ".v2-sync-log {" in styles
|
||||
assert "border-radius: var(--ui-radius-section) !important" in styles
|
||||
|
||||
|
||||
def test_assistant_composer_hint_stays_inside_narrow_input() -> None:
|
||||
styles = source("ui-consistency.css")
|
||||
|
||||
assert ".v2-request-form .composer-hint" in styles
|
||||
assert "left: 16px !important" in styles
|
||||
assert "right: auto !important" in styles
|
||||
assert "max-width: calc(100% - 132px)" in styles
|
||||
|
||||
|
||||
def test_failed_delivery_uses_one_semantic_status_treatment() -> None:
|
||||
styles = source("ui-consistency.css")
|
||||
|
||||
assert '[data-delivery-state="blocked"]' in styles
|
||||
assert '[data-delivery-state="failed"]' in styles
|
||||
assert "color: var(--destructive)" in styles
|
||||
@@ -0,0 +1,85 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
WEB = ROOT / "apps" / "web-console"
|
||||
|
||||
|
||||
def source(name: str) -> str:
|
||||
return (WEB / name).read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_vue_presentation_assets_are_local_ordered_and_cached() -> None:
|
||||
html = source("index.html")
|
||||
service_worker = source("service-worker.js")
|
||||
runtime = "/vendor/vue/3.5.21/vue.runtime.global.prod.js"
|
||||
presentation = "/vue-ui.js?v=vue-presentation-3"
|
||||
|
||||
assert html.index(runtime) < html.index(presentation) < html.index(
|
||||
"/app.js?v=dual-seat-135"
|
||||
)
|
||||
assert html.index(presentation) < html.index(
|
||||
"/assistant-chat.js?v=codex-assistant-32"
|
||||
)
|
||||
assert f'"{runtime}"' in service_worker
|
||||
assert f'"{presentation}"' in service_worker
|
||||
assert 'const CACHE_VERSION = "cognitive-agent-pad-v290"' in service_worker
|
||||
assert "unpkg.com" not in html
|
||||
assert "cdn.jsdelivr.net" not in html
|
||||
|
||||
|
||||
def test_vendored_vue_runtime_and_license_are_present() -> None:
|
||||
runtime = WEB / "vendor" / "vue" / "3.5.21" / "vue.runtime.global.prod.js"
|
||||
license_file = WEB / "vendor" / "vue" / "3.5.21" / "LICENSE"
|
||||
|
||||
assert runtime.is_file()
|
||||
assert runtime.stat().st_size > 50_000
|
||||
assert "vue v3.5.21" in runtime.read_text(encoding="utf-8")[:500]
|
||||
assert license_file.is_file()
|
||||
assert "The MIT License" in license_file.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_vue_islands_render_modes_and_dispatched_controls() -> None:
|
||||
vue_ui = source("vue-ui.js")
|
||||
app = source("app.js")
|
||||
assistant = source("assistant-chat.js")
|
||||
styles = source("assistant-chat.css")
|
||||
|
||||
assert 'name: "N7ModeChipRail"' in vue_ui
|
||||
assert 'name: "N7DispatchedControlsCard"' in vue_ui
|
||||
assert 'container.dataset.vueSurface = "mode-chips"' in vue_ui
|
||||
assert 'container.dataset.vueSurface = "dispatched-controls"' in vue_ui
|
||||
assert "renderModeChips" in vue_ui
|
||||
assert "renderDispatchedControls" in vue_ui
|
||||
assert "window.CognitiveAgentVueUI = Object.freeze" in vue_ui
|
||||
assert '"data-mode-run": "true"' in vue_ui
|
||||
|
||||
assert "function vueModeChipEntries(modes)" in app
|
||||
assert "window.CognitiveAgentVueUI?.renderModeChips?.(" in app
|
||||
assert "window.CognitiveAgentVueUI?.renderDispatchedControls" in assistant
|
||||
assert 'host.className = "assistant-dispatched-controls-vue-host"' in assistant
|
||||
assert "deliveryLabel: String(item.deliveryLabel" in assistant
|
||||
assert "deliveryDetail: String(item.deliveryDetail" in assistant
|
||||
assert "下发状态:" in vue_ui
|
||||
assert ".assistant-dispatched-controls-vue-host" in styles
|
||||
|
||||
|
||||
def test_existing_dom_renderers_remain_as_no_vue_fallbacks() -> None:
|
||||
app = source("app.js")
|
||||
assistant = source("assistant-chat.js")
|
||||
|
||||
mode_vue_call = app.index("window.CognitiveAgentVueUI?.renderModeChips?.(")
|
||||
mode_fallback = app.index("const runtimeModeIds = new Set", mode_vue_call)
|
||||
assert mode_fallback > mode_vue_call
|
||||
|
||||
command_vue_call = assistant.index(
|
||||
"window.CognitiveAgentVueUI?.renderDispatchedControls"
|
||||
)
|
||||
command_fallback = assistant.index(
|
||||
'document.createElement("details")', command_vue_call
|
||||
)
|
||||
assert command_fallback > command_vue_call
|
||||
assert "renderDispatchedControls: () => false" in source("vue-ui.js")
|
||||
assert "renderModeChips: () => false" in source("vue-ui.js")
|
||||
@@ -0,0 +1,37 @@
|
||||
import { FileBlob, SpreadsheetFile } from "@oai/artifact-tool";
|
||||
|
||||
const workbookPath = "D:/N7Web-Code/Cognitive_Agent/data/空调-天幕-氛围灯CAN信号矩阵表.xlsx";
|
||||
const input = await FileBlob.load(workbookPath);
|
||||
const workbook = await SpreadsheetFile.importXlsx(input);
|
||||
|
||||
const summary = await workbook.inspect({
|
||||
kind: "workbook,sheet,table",
|
||||
maxChars: 12000,
|
||||
tableMaxRows: 8,
|
||||
tableMaxCols: 20,
|
||||
tableMaxCellChars: 120,
|
||||
});
|
||||
console.log("SUMMARY");
|
||||
console.log(summary.ndjson);
|
||||
|
||||
const matches = await workbook.inspect({
|
||||
kind: "match",
|
||||
searchTerm: "42E|氛围|Ambient|颜色|色号|亮度",
|
||||
options: { useRegex: true, maxResults: 300 },
|
||||
maxChars: 30000,
|
||||
});
|
||||
console.log("MATCHES");
|
||||
console.log(matches.ndjson);
|
||||
|
||||
const ambient42e = await workbook.inspect({
|
||||
kind: "table",
|
||||
sheetId: "氛围灯",
|
||||
range: "A10:K27",
|
||||
include: "values,formulas",
|
||||
tableMaxRows: 30,
|
||||
tableMaxCols: 11,
|
||||
tableMaxCellChars: 500,
|
||||
maxChars: 30000,
|
||||
});
|
||||
console.log("AMBIENT_42E");
|
||||
console.log(ambient42e.ndjson);
|
||||
+261
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"hoistPattern": [
|
||||
"*"
|
||||
],
|
||||
"included": {
|
||||
"dependencies": true,
|
||||
"devDependencies": false,
|
||||
"optionalDependencies": true
|
||||
},
|
||||
"injectedDeps": {},
|
||||
"ignoredBuilds": [],
|
||||
"layoutVersion": 5,
|
||||
"hoistedLocations": {
|
||||
"@napi-rs/canvas@0.1.100": [
|
||||
"node_modules\\@napi-rs\\canvas"
|
||||
],
|
||||
"@oai/artifact-tool@file:local-deps/-oai-artifact-tool-oai-artifact_tool-2.8.33.tgz": [
|
||||
"node_modules\\@oai\\artifact-tool"
|
||||
],
|
||||
"@viz-js/viz@3.25.0": [
|
||||
"node_modules\\@viz-js\\viz"
|
||||
],
|
||||
"docx@9.6.1": [
|
||||
"node_modules\\docx"
|
||||
],
|
||||
"jpeg-js@0.4.4": [
|
||||
"node_modules\\jpeg-js"
|
||||
],
|
||||
"lucide@1.8.0": [
|
||||
"node_modules\\lucide"
|
||||
],
|
||||
"marked@17.0.5": [
|
||||
"node_modules\\marked"
|
||||
],
|
||||
"pdf-lib@1.17.1": [
|
||||
"node_modules\\pdf-lib"
|
||||
],
|
||||
"pdfjs-dist@5.6.205": [
|
||||
"node_modules\\pdfjs-dist"
|
||||
],
|
||||
"pixelmatch@7.2.0": [
|
||||
"node_modules\\pixelmatch"
|
||||
],
|
||||
"playwright@1.61.1": [
|
||||
"node_modules\\playwright"
|
||||
],
|
||||
"pngjs@7.0.0": [
|
||||
"node_modules\\pngjs"
|
||||
],
|
||||
"pnpm@11.9.0": [
|
||||
"node_modules\\pnpm"
|
||||
],
|
||||
"@types/node@22.20.1": [
|
||||
"node_modules\\pptxgenjs\\node_modules\\@types\\node"
|
||||
],
|
||||
"undici-types@6.21.0": [
|
||||
"node_modules\\pptxgenjs\\node_modules\\undici-types"
|
||||
],
|
||||
"sharp@0.34.5": [
|
||||
"node_modules\\sharp"
|
||||
],
|
||||
"tesseract.js@7.0.0": [
|
||||
"node_modules\\tesseract.js"
|
||||
],
|
||||
"@napi-rs/canvas-win32-x64-msvc@0.1.100": [
|
||||
"node_modules\\@napi-rs\\canvas-win32-x64-msvc"
|
||||
],
|
||||
"@types/node@25.9.5": [
|
||||
"node_modules\\@types\\node"
|
||||
],
|
||||
"hash.js@1.1.7": [
|
||||
"node_modules\\hash.js"
|
||||
],
|
||||
"jszip@3.10.1": [
|
||||
"node_modules\\jszip"
|
||||
],
|
||||
"nanoid@5.1.16": [
|
||||
"node_modules\\nanoid"
|
||||
],
|
||||
"xml@1.0.1": [
|
||||
"node_modules\\xml"
|
||||
],
|
||||
"xml-js@1.6.11": [
|
||||
"node_modules\\xml-js"
|
||||
],
|
||||
"@pdf-lib/standard-fonts@1.0.0": [
|
||||
"node_modules\\@pdf-lib\\standard-fonts"
|
||||
],
|
||||
"@pdf-lib/upng@1.0.1": [
|
||||
"node_modules\\@pdf-lib\\upng"
|
||||
],
|
||||
"pako@1.0.11": [
|
||||
"node_modules\\pako"
|
||||
],
|
||||
"tslib@1.14.1": [
|
||||
"node_modules\\tslib"
|
||||
],
|
||||
"node-readable-to-web-readable-stream@0.4.2": [
|
||||
"node_modules\\node-readable-to-web-readable-stream"
|
||||
],
|
||||
"playwright-core@1.61.1": [
|
||||
"node_modules\\playwright-core"
|
||||
],
|
||||
"https@1.0.0": [
|
||||
"node_modules\\https"
|
||||
],
|
||||
"image-size@1.2.1": [
|
||||
"node_modules\\image-size"
|
||||
],
|
||||
"@img/colour@1.1.0": [
|
||||
"node_modules\\@img\\colour"
|
||||
],
|
||||
"detect-libc@2.1.2": [
|
||||
"node_modules\\detect-libc"
|
||||
],
|
||||
"semver@7.8.5": [
|
||||
"node_modules\\semver"
|
||||
],
|
||||
"@img/sharp-win32-x64@0.34.5": [
|
||||
"node_modules\\@img\\sharp-win32-x64"
|
||||
],
|
||||
"bmp-js@0.1.0": [
|
||||
"node_modules\\bmp-js"
|
||||
],
|
||||
"idb-keyval@6.3.0": [
|
||||
"node_modules\\idb-keyval"
|
||||
],
|
||||
"is-url@1.2.4": [
|
||||
"node_modules\\is-url"
|
||||
],
|
||||
"node-fetch@2.7.0": [
|
||||
"node_modules\\node-fetch"
|
||||
],
|
||||
"opencollective-postinstall@2.0.3": [
|
||||
"node_modules\\opencollective-postinstall"
|
||||
],
|
||||
"regenerator-runtime@0.13.11": [
|
||||
"node_modules\\regenerator-runtime"
|
||||
],
|
||||
"tesseract.js-core@7.0.0": [
|
||||
"node_modules\\tesseract.js-core"
|
||||
],
|
||||
"wasm-feature-detect@1.8.0": [
|
||||
"node_modules\\wasm-feature-detect"
|
||||
],
|
||||
"zlibjs@0.3.1": [
|
||||
"node_modules\\zlibjs"
|
||||
],
|
||||
"undici-types@7.24.6": [
|
||||
"node_modules\\undici-types"
|
||||
],
|
||||
"inherits@2.0.4": [
|
||||
"node_modules\\inherits"
|
||||
],
|
||||
"minimalistic-assert@1.0.1": [
|
||||
"node_modules\\minimalistic-assert"
|
||||
],
|
||||
"lie@3.3.0": [
|
||||
"node_modules\\lie"
|
||||
],
|
||||
"readable-stream@2.3.8": [
|
||||
"node_modules\\readable-stream"
|
||||
],
|
||||
"setimmediate@1.0.5": [
|
||||
"node_modules\\setimmediate"
|
||||
],
|
||||
"sax@1.6.0": [
|
||||
"node_modules\\sax"
|
||||
],
|
||||
"queue@6.0.2": [
|
||||
"node_modules\\queue"
|
||||
],
|
||||
"whatwg-url@5.0.0": [
|
||||
"node_modules\\whatwg-url"
|
||||
],
|
||||
"immediate@3.0.6": [
|
||||
"node_modules\\immediate"
|
||||
],
|
||||
"core-util-is@1.0.3": [
|
||||
"node_modules\\core-util-is"
|
||||
],
|
||||
"isarray@1.0.0": [
|
||||
"node_modules\\isarray"
|
||||
],
|
||||
"process-nextick-args@2.0.1": [
|
||||
"node_modules\\process-nextick-args"
|
||||
],
|
||||
"safe-buffer@5.1.2": [
|
||||
"node_modules\\safe-buffer"
|
||||
],
|
||||
"string_decoder@1.1.1": [
|
||||
"node_modules\\string_decoder"
|
||||
],
|
||||
"util-deprecate@1.0.2": [
|
||||
"node_modules\\util-deprecate"
|
||||
],
|
||||
"tr46@0.0.3": [
|
||||
"node_modules\\tr46"
|
||||
],
|
||||
"webidl-conversions@3.0.1": [
|
||||
"node_modules\\webidl-conversions"
|
||||
],
|
||||
"pptxgenjs@4.0.1": [
|
||||
"node_modules\\pptxgenjs"
|
||||
]
|
||||
},
|
||||
"nodeLinker": "hoisted",
|
||||
"packageManager": "pnpm@11.9.0",
|
||||
"pendingBuilds": [],
|
||||
"publicHoistPattern": [],
|
||||
"prunedAt": "Mon, 27 Jul 2026 21:49:51 GMT",
|
||||
"registries": {
|
||||
"default": "https://openai.firewall.socket.dev/npm/",
|
||||
"@jsr": "https://npm.jsr.io/"
|
||||
},
|
||||
"skipped": [
|
||||
"@emnapi/runtime@1.11.2",
|
||||
"@img/sharp-darwin-arm64@0.34.5",
|
||||
"@img/sharp-darwin-x64@0.34.5",
|
||||
"@img/sharp-libvips-darwin-arm64@1.2.4",
|
||||
"@img/sharp-libvips-darwin-x64@1.2.4",
|
||||
"@img/sharp-libvips-linux-arm64@1.2.4",
|
||||
"@img/sharp-libvips-linux-arm@1.2.4",
|
||||
"@img/sharp-libvips-linux-ppc64@1.2.4",
|
||||
"@img/sharp-libvips-linux-riscv64@1.2.4",
|
||||
"@img/sharp-libvips-linux-s390x@1.2.4",
|
||||
"@img/sharp-libvips-linux-x64@1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-arm64@1.2.4",
|
||||
"@img/sharp-libvips-linuxmusl-x64@1.2.4",
|
||||
"@img/sharp-linux-arm64@0.34.5",
|
||||
"@img/sharp-linux-arm@0.34.5",
|
||||
"@img/sharp-linux-ppc64@0.34.5",
|
||||
"@img/sharp-linux-riscv64@0.34.5",
|
||||
"@img/sharp-linux-s390x@0.34.5",
|
||||
"@img/sharp-linux-x64@0.34.5",
|
||||
"@img/sharp-linuxmusl-arm64@0.34.5",
|
||||
"@img/sharp-linuxmusl-x64@0.34.5",
|
||||
"@img/sharp-wasm32@0.34.5",
|
||||
"@img/sharp-win32-arm64@0.34.5",
|
||||
"@img/sharp-win32-ia32@0.34.5",
|
||||
"@napi-rs/canvas-android-arm64@0.1.100",
|
||||
"@napi-rs/canvas-darwin-arm64@0.1.100",
|
||||
"@napi-rs/canvas-darwin-x64@0.1.100",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf@0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-gnu@0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-musl@0.1.100",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu@0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-gnu@0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-musl@0.1.100",
|
||||
"@napi-rs/canvas-win32-arm64-msvc@0.1.100",
|
||||
"fsevents@2.3.2",
|
||||
"tslib@2.8.1"
|
||||
],
|
||||
"storeDir": "C:\\Users\\runneradmin\\AppData\\Local\\pnpm\\store\\v11",
|
||||
"virtualStoreDir": "C:\\Users\\runneradmin\\AppData\\Local\\Temp\\codex-primary-runtime-ixpDI1\\install\\node_modules\\.pnpm",
|
||||
"virtualStoreDirMaxLength": 60,
|
||||
"allowBuilds": {
|
||||
"sharp": true,
|
||||
"tesseract.js": true
|
||||
}
|
||||
}
|
||||
+1
File diff suppressed because one or more lines are too long
+49
@@ -0,0 +1,49 @@
|
||||
{
|
||||
"lastValidatedTimestamp": 1785188991196,
|
||||
"projects": {
|
||||
"C:\\Users\\runneradmin\\AppData\\Local\\Temp\\codex-primary-runtime-ixpDI1\\install": {
|
||||
"name": "@openai/codex-electron-bundled-runtimes",
|
||||
"version": "0.0.0"
|
||||
}
|
||||
},
|
||||
"pnpmfiles": [],
|
||||
"settings": {
|
||||
"enableGlobalVirtualStore": false,
|
||||
"allowBuilds": {
|
||||
"sharp": true,
|
||||
"tesseract.js": true
|
||||
},
|
||||
"autoInstallPeers": true,
|
||||
"catalogs": {},
|
||||
"dedupeDirectDeps": false,
|
||||
"dedupeInjectedDeps": true,
|
||||
"dedupePeerDependents": true,
|
||||
"dedupePeers": false,
|
||||
"dev": false,
|
||||
"excludeLinksFromLockfile": false,
|
||||
"hoistPattern": [
|
||||
"*"
|
||||
],
|
||||
"hoistWorkspacePackages": true,
|
||||
"injectWorkspacePackages": false,
|
||||
"linkWorkspacePackages": false,
|
||||
"minimumReleaseAge": "10080",
|
||||
"minimumReleaseAgeStrict": true,
|
||||
"minimumReleaseAgeExclude": [
|
||||
"marked",
|
||||
"playwright-core",
|
||||
"pnpm"
|
||||
],
|
||||
"minimumReleaseAgeIgnoreMissingTime": true,
|
||||
"nodeLinker": "hoisted",
|
||||
"optional": true,
|
||||
"peersSuffixMaxLength": 1000,
|
||||
"preferWorkspacePackages": false,
|
||||
"production": true,
|
||||
"publicHoistPattern": [],
|
||||
"workspacePackagePatterns": [
|
||||
"."
|
||||
]
|
||||
},
|
||||
"filteredInstall": false
|
||||
}
|
||||
+896
@@ -0,0 +1,896 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
'@napi-rs/canvas':
|
||||
specifier: ^0.1.91
|
||||
version: 0.1.100
|
||||
'@oai/artifact-tool':
|
||||
specifier: file:./local-deps/-oai-artifact-tool-oai-artifact_tool-2.8.33.tgz
|
||||
version: file:local-deps/-oai-artifact-tool-oai-artifact_tool-2.8.33.tgz
|
||||
'@viz-js/viz':
|
||||
specifier: 3.25.0
|
||||
version: 3.25.0
|
||||
docx:
|
||||
specifier: 9.6.1
|
||||
version: 9.6.1
|
||||
jpeg-js:
|
||||
specifier: ^0.4.4
|
||||
version: 0.4.4
|
||||
lucide:
|
||||
specifier: 1.8.0
|
||||
version: 1.8.0
|
||||
marked:
|
||||
specifier: 17.0.5
|
||||
version: 17.0.5
|
||||
pdf-lib:
|
||||
specifier: 1.17.1
|
||||
version: 1.17.1
|
||||
pdfjs-dist:
|
||||
specifier: 5.6.205
|
||||
version: 5.6.205
|
||||
pixelmatch:
|
||||
specifier: ^7.1.0
|
||||
version: 7.2.0
|
||||
playwright:
|
||||
specifier: ^1.57.0
|
||||
version: 1.61.1
|
||||
pngjs:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
pnpm:
|
||||
specifier: 11.9.0
|
||||
version: 11.9.0
|
||||
pptxgenjs:
|
||||
specifier: 4.0.1
|
||||
version: 4.0.1
|
||||
sharp:
|
||||
specifier: 0.34.5
|
||||
version: 0.34.5
|
||||
tesseract.js:
|
||||
specifier: ^7.0.0
|
||||
version: 7.0.0
|
||||
|
||||
packages:
|
||||
|
||||
'@emnapi/runtime@1.11.2':
|
||||
resolution: {integrity: sha512-kyOl3X0DuTiT1h2ft8r2fYO8JYtU9a9Xis/zBSiGArNaagCOWx90N1k2wxp18czFDH+OgcWGb5ZP/XMt3dcyPA==}
|
||||
|
||||
'@img/colour@1.1.0':
|
||||
resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==}
|
||||
engines: {node: '>= 10'}
|
||||
|
||||
'@oai/artifact-tool@file:local-deps/-oai-artifact-tool-oai-artifact_tool-2.8.33.tgz':
|
||||
resolution: {integrity: sha512-4c2SpNNLIWg9lCE7m+LlbuL52KqV+TqYk7AmF84dggaEvORkO2DYecbLDTHANhu2IkOmKzUQPVm9KGrODUB8pQ==, tarball: file:local-deps/-oai-artifact-tool-oai-artifact_tool-2.8.33.tgz}
|
||||
version: 2.8.33
|
||||
bundledDependencies:
|
||||
- '@oai/embedded-fonts'
|
||||
- '@oai/walnut'
|
||||
- skia-canvas
|
||||
|
||||
'@pdf-lib/standard-fonts@1.0.0':
|
||||
resolution: {integrity: sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==}
|
||||
|
||||
'@pdf-lib/upng@1.0.1':
|
||||
resolution: {integrity: sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==}
|
||||
|
||||
'@types/node@22.20.1':
|
||||
resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==}
|
||||
|
||||
'@types/node@25.9.5':
|
||||
resolution: {integrity: sha512-OScDchr2fwuUmWdf4kZ9h7PcJiYDVInhJizG/biAq3cAvqwYktuy/TYGGdZNMtNTFUP7rnb0NU4TUdm82kt4Rg==}
|
||||
|
||||
'@viz-js/viz@3.25.0':
|
||||
resolution: {integrity: sha512-dM7zAYMdf7mcRz5Kdb+YJb6+qv5Rjk0rPZ18gROdpMrP/3S7RFOp8uxybeiz5RypHrE1zo1vccA8Twh4mIcLZw==}
|
||||
|
||||
bmp-js@0.1.0:
|
||||
resolution: {integrity: sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==}
|
||||
|
||||
core-util-is@1.0.3:
|
||||
resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
docx@9.6.1:
|
||||
resolution: {integrity: sha512-ZJja9/KBUuFC109sCMzovoq2GR2wCG/AuxivjA+OHj/q0TEgJIm3S7yrlUxIy3B+bV8YDj/BiHfWyrRFmyWpDQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
fsevents@2.3.2:
|
||||
resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
hash.js@1.1.7:
|
||||
resolution: {integrity: sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==}
|
||||
|
||||
https@1.0.0:
|
||||
resolution: {integrity: sha512-4EC57ddXrkaF0x83Oj8sM6SLQHAWXw90Skqu2M4AEWENZ3F02dFJE/GARA8igO79tcgYqGrD7ae4f5L3um2lgg==}
|
||||
|
||||
idb-keyval@6.3.0:
|
||||
resolution: {integrity: sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==}
|
||||
|
||||
image-size@1.2.1:
|
||||
resolution: {integrity: sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==}
|
||||
engines: {node: '>=16.x'}
|
||||
hasBin: true
|
||||
|
||||
immediate@3.0.6:
|
||||
resolution: {integrity: sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==}
|
||||
|
||||
inherits@2.0.4:
|
||||
resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==}
|
||||
|
||||
is-url@1.2.4:
|
||||
resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==}
|
||||
|
||||
isarray@1.0.0:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
jpeg-js@0.4.4:
|
||||
resolution: {integrity: sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==}
|
||||
|
||||
jszip@3.10.1:
|
||||
resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==}
|
||||
|
||||
lie@3.3.0:
|
||||
resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==}
|
||||
|
||||
lucide@1.8.0:
|
||||
resolution: {integrity: sha512-JjV/QnadgFLj1Pyu9IKl0lknrolFEzo04B64QcYLLeRzZl/iEHpdbSrRRKbyXcv45SZNv+WGjIUCT33e7xHO6Q==}
|
||||
|
||||
marked@17.0.5:
|
||||
resolution: {integrity: sha512-6hLvc0/JEbRjRgzI6wnT2P1XuM1/RrrDEX0kPt0N7jGm1133g6X7DlxFasUIx+72aKAr904GTxhSLDrd5DIlZg==}
|
||||
engines: {node: '>= 20'}
|
||||
hasBin: true
|
||||
|
||||
minimalistic-assert@1.0.1:
|
||||
resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
|
||||
|
||||
nanoid@5.1.16:
|
||||
resolution: {integrity: sha512-kVrnsrJqMR8+oLJnGEmSWw9BivK5mt7H3FZatVRjrc5wGqFYuBxX1yG7+A7Gi5AefkX6t/oCkizcQgpu0cY1dQ==}
|
||||
engines: {node: ^18 || >=20}
|
||||
hasBin: true
|
||||
|
||||
node-fetch@2.7.0:
|
||||
resolution: {integrity: sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==}
|
||||
engines: {node: 4.x || >=6.0.0}
|
||||
peerDependencies:
|
||||
encoding: ^0.1.0
|
||||
peerDependenciesMeta:
|
||||
encoding:
|
||||
optional: true
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==}
|
||||
|
||||
opencollective-postinstall@2.0.3:
|
||||
resolution: {integrity: sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==}
|
||||
hasBin: true
|
||||
|
||||
pako@1.0.11:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
pdf-lib@1.17.1:
|
||||
resolution: {integrity: sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==}
|
||||
|
||||
pdfjs-dist@5.6.205:
|
||||
resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==}
|
||||
engines: {node: '>=20.19.0 || >=22.13.0 || >=24'}
|
||||
|
||||
pixelmatch@7.2.0:
|
||||
resolution: {integrity: sha512-xhcb4yHu9sM/G7foGzoLtXYcC0zHEaOXXjRKhGup0fw78Nf2Tkiapv4EQyMzrbcmQPsllAI7DbFY2UT7PlI9Pg==}
|
||||
hasBin: true
|
||||
|
||||
playwright-core@1.61.1:
|
||||
resolution: {integrity: sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
playwright@1.61.1:
|
||||
resolution: {integrity: sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
pngjs@7.0.0:
|
||||
resolution: {integrity: sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==}
|
||||
engines: {node: '>=14.19.0'}
|
||||
|
||||
pnpm@11.9.0:
|
||||
resolution: {integrity: sha512-vWgtXQP+Ul73yf1ngMaITR51asTJyf4AxTh4KCQxDc+Q493E9Tg18G3669UIXkGFXgvLs7YN4qxburieUDbwOw==}
|
||||
engines: {node: '>=22.13'}
|
||||
hasBin: true
|
||||
|
||||
pptxgenjs@4.0.1:
|
||||
resolution: {integrity: sha512-TeJISr8wouAuXw4C1F/mC33xbZs/FuEG6nH9FG1Zj+nuPcGMP5YRHl6X+j3HSUnS1f3at6k75ZZXPMZlA5Lj9A==}
|
||||
|
||||
process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
queue@6.0.2:
|
||||
resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==}
|
||||
|
||||
readable-stream@2.3.8:
|
||||
resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==}
|
||||
|
||||
regenerator-runtime@0.13.11:
|
||||
resolution: {integrity: sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==}
|
||||
|
||||
safe-buffer@5.1.2:
|
||||
resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==}
|
||||
|
||||
sax@1.6.0:
|
||||
resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==}
|
||||
engines: {node: '>=11.0.0'}
|
||||
|
||||
semver@7.8.5:
|
||||
resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
setimmediate@1.0.5:
|
||||
resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==}
|
||||
|
||||
sharp@0.34.5:
|
||||
resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
string_decoder@1.1.1:
|
||||
resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==}
|
||||
|
||||
tesseract.js-core@7.0.0:
|
||||
resolution: {integrity: sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==}
|
||||
|
||||
tesseract.js@7.0.0:
|
||||
resolution: {integrity: sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==}
|
||||
|
||||
tr46@0.0.3:
|
||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||
|
||||
tslib@1.14.1:
|
||||
resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
undici-types@6.21.0:
|
||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||
|
||||
undici-types@7.24.6:
|
||||
resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==}
|
||||
|
||||
util-deprecate@1.0.2:
|
||||
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
|
||||
|
||||
wasm-feature-detect@1.8.0:
|
||||
resolution: {integrity: sha512-zksaLKM2fVlnB5jQQDqKXXwYHLQUVH9es+5TOOHwGOVJOCeRBCiPjwSg+3tN2AdTCzjgli4jijCH290kXb/zWQ==}
|
||||
|
||||
webidl-conversions@3.0.1:
|
||||
resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
|
||||
|
||||
xml-js@1.6.11:
|
||||
resolution: {integrity: sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==}
|
||||
hasBin: true
|
||||
|
||||
xml@1.0.1:
|
||||
resolution: {integrity: sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==}
|
||||
|
||||
zlibjs@0.3.1:
|
||||
resolution: {integrity: sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@emnapi/runtime@1.11.2':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@img/colour@1.1.0': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-ppc64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-riscv64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.2.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-ppc64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-riscv64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.34.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.34.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.11.2
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-arm64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-android-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-arm64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-darwin-x64@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-arm64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-riscv64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-gnu@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-linux-x64-musl@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-arm64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas-win32-x64-msvc@0.1.100':
|
||||
optional: true
|
||||
|
||||
'@napi-rs/canvas@0.1.100':
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas-android-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-arm64': 0.1.100
|
||||
'@napi-rs/canvas-darwin-x64': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-arm64-musl': 0.1.100
|
||||
'@napi-rs/canvas-linux-riscv64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-gnu': 0.1.100
|
||||
'@napi-rs/canvas-linux-x64-musl': 0.1.100
|
||||
'@napi-rs/canvas-win32-arm64-msvc': 0.1.100
|
||||
'@napi-rs/canvas-win32-x64-msvc': 0.1.100
|
||||
|
||||
'@oai/artifact-tool@file:local-deps/-oai-artifact-tool-oai-artifact_tool-2.8.33.tgz': {}
|
||||
|
||||
'@pdf-lib/standard-fonts@1.0.0':
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
|
||||
'@pdf-lib/upng@1.0.1':
|
||||
dependencies:
|
||||
pako: 1.0.11
|
||||
|
||||
'@types/node@22.20.1':
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/node@25.9.5':
|
||||
dependencies:
|
||||
undici-types: 7.24.6
|
||||
|
||||
'@viz-js/viz@3.25.0': {}
|
||||
|
||||
bmp-js@0.1.0: {}
|
||||
|
||||
core-util-is@1.0.3: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
docx@9.6.1:
|
||||
dependencies:
|
||||
'@types/node': 25.9.5
|
||||
hash.js: 1.1.7
|
||||
jszip: 3.10.1
|
||||
nanoid: 5.1.16
|
||||
xml: 1.0.1
|
||||
xml-js: 1.6.11
|
||||
|
||||
fsevents@2.3.2:
|
||||
optional: true
|
||||
|
||||
hash.js@1.1.7:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
minimalistic-assert: 1.0.1
|
||||
|
||||
https@1.0.0: {}
|
||||
|
||||
idb-keyval@6.3.0: {}
|
||||
|
||||
image-size@1.2.1:
|
||||
dependencies:
|
||||
queue: 6.0.2
|
||||
|
||||
immediate@3.0.6: {}
|
||||
|
||||
inherits@2.0.4: {}
|
||||
|
||||
is-url@1.2.4: {}
|
||||
|
||||
isarray@1.0.0: {}
|
||||
|
||||
jpeg-js@0.4.4: {}
|
||||
|
||||
jszip@3.10.1:
|
||||
dependencies:
|
||||
lie: 3.3.0
|
||||
pako: 1.0.11
|
||||
readable-stream: 2.3.8
|
||||
setimmediate: 1.0.5
|
||||
|
||||
lie@3.3.0:
|
||||
dependencies:
|
||||
immediate: 3.0.6
|
||||
|
||||
lucide@1.8.0: {}
|
||||
|
||||
marked@17.0.5: {}
|
||||
|
||||
minimalistic-assert@1.0.1: {}
|
||||
|
||||
nanoid@5.1.16: {}
|
||||
|
||||
node-fetch@2.7.0:
|
||||
dependencies:
|
||||
whatwg-url: 5.0.0
|
||||
|
||||
node-readable-to-web-readable-stream@0.4.2:
|
||||
optional: true
|
||||
|
||||
opencollective-postinstall@2.0.3: {}
|
||||
|
||||
pako@1.0.11: {}
|
||||
|
||||
pdf-lib@1.17.1:
|
||||
dependencies:
|
||||
'@pdf-lib/standard-fonts': 1.0.0
|
||||
'@pdf-lib/upng': 1.0.1
|
||||
pako: 1.0.11
|
||||
tslib: 1.14.1
|
||||
|
||||
pdfjs-dist@5.6.205:
|
||||
optionalDependencies:
|
||||
'@napi-rs/canvas': 0.1.100
|
||||
node-readable-to-web-readable-stream: 0.4.2
|
||||
|
||||
pixelmatch@7.2.0:
|
||||
dependencies:
|
||||
pngjs: 7.0.0
|
||||
|
||||
playwright-core@1.61.1: {}
|
||||
|
||||
playwright@1.61.1:
|
||||
dependencies:
|
||||
playwright-core: 1.61.1
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.2
|
||||
|
||||
pngjs@7.0.0: {}
|
||||
|
||||
pnpm@11.9.0: {}
|
||||
|
||||
pptxgenjs@4.0.1:
|
||||
dependencies:
|
||||
'@types/node': 22.20.1
|
||||
https: 1.0.0
|
||||
image-size: 1.2.1
|
||||
jszip: 3.10.1
|
||||
|
||||
process-nextick-args@2.0.1: {}
|
||||
|
||||
queue@6.0.2:
|
||||
dependencies:
|
||||
inherits: 2.0.4
|
||||
|
||||
readable-stream@2.3.8:
|
||||
dependencies:
|
||||
core-util-is: 1.0.3
|
||||
inherits: 2.0.4
|
||||
isarray: 1.0.0
|
||||
process-nextick-args: 2.0.1
|
||||
safe-buffer: 5.1.2
|
||||
string_decoder: 1.1.1
|
||||
util-deprecate: 1.0.2
|
||||
|
||||
regenerator-runtime@0.13.11: {}
|
||||
|
||||
safe-buffer@5.1.2: {}
|
||||
|
||||
sax@1.6.0: {}
|
||||
|
||||
semver@7.8.5: {}
|
||||
|
||||
setimmediate@1.0.5: {}
|
||||
|
||||
sharp@0.34.5:
|
||||
dependencies:
|
||||
'@img/colour': 1.1.0
|
||||
detect-libc: 2.1.2
|
||||
semver: 7.8.5
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.34.5
|
||||
'@img/sharp-darwin-x64': 0.34.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.2.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm': 1.2.4
|
||||
'@img/sharp-libvips-linux-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linux-ppc64': 1.2.4
|
||||
'@img/sharp-libvips-linux-riscv64': 1.2.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.2.4
|
||||
'@img/sharp-libvips-linux-x64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.2.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.2.4
|
||||
'@img/sharp-linux-arm': 0.34.5
|
||||
'@img/sharp-linux-arm64': 0.34.5
|
||||
'@img/sharp-linux-ppc64': 0.34.5
|
||||
'@img/sharp-linux-riscv64': 0.34.5
|
||||
'@img/sharp-linux-s390x': 0.34.5
|
||||
'@img/sharp-linux-x64': 0.34.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.34.5
|
||||
'@img/sharp-linuxmusl-x64': 0.34.5
|
||||
'@img/sharp-wasm32': 0.34.5
|
||||
'@img/sharp-win32-arm64': 0.34.5
|
||||
'@img/sharp-win32-ia32': 0.34.5
|
||||
'@img/sharp-win32-x64': 0.34.5
|
||||
|
||||
string_decoder@1.1.1:
|
||||
dependencies:
|
||||
safe-buffer: 5.1.2
|
||||
|
||||
tesseract.js-core@7.0.0: {}
|
||||
|
||||
tesseract.js@7.0.0:
|
||||
dependencies:
|
||||
bmp-js: 0.1.0
|
||||
idb-keyval: 6.3.0
|
||||
is-url: 1.2.4
|
||||
node-fetch: 2.7.0
|
||||
opencollective-postinstall: 2.0.3
|
||||
regenerator-runtime: 0.13.11
|
||||
tesseract.js-core: 7.0.0
|
||||
wasm-feature-detect: 1.8.0
|
||||
zlibjs: 0.3.1
|
||||
transitivePeerDependencies:
|
||||
- encoding
|
||||
|
||||
tr46@0.0.3: {}
|
||||
|
||||
tslib@1.14.1: {}
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
undici-types@6.21.0: {}
|
||||
|
||||
undici-types@7.24.6: {}
|
||||
|
||||
util-deprecate@1.0.2: {}
|
||||
|
||||
wasm-feature-detect@1.8.0: {}
|
||||
|
||||
webidl-conversions@3.0.1: {}
|
||||
|
||||
whatwg-url@5.0.0:
|
||||
dependencies:
|
||||
tr46: 0.0.3
|
||||
webidl-conversions: 3.0.1
|
||||
|
||||
xml-js@1.6.11:
|
||||
dependencies:
|
||||
sax: 1.6.0
|
||||
|
||||
xml@1.0.1: {}
|
||||
|
||||
zlibjs@0.3.1: {}
|
||||
+82
@@ -0,0 +1,82 @@
|
||||
# Licensing
|
||||
|
||||
## color
|
||||
|
||||
Copyright (c) 2012 Heather Arthur
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## color-convert
|
||||
|
||||
Copyright (c) 2011-2016 Heather Arthur <fayearthur@gmail.com>.
|
||||
Copyright (c) 2016-2021 Josh Junon <josh@junon.me>.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## color-string
|
||||
|
||||
Copyright (c) 2011 Heather Arthur <fayearthur@gmail.com>
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
|
||||
## color-name
|
||||
|
||||
The MIT License (MIT)
|
||||
Copyright (c) 2015 Dmitry Ivanov
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
# `@img/colour`
|
||||
|
||||
The latest version of the
|
||||
[color](https://www.npmjs.com/package/color)
|
||||
package is now ESM-only,
|
||||
however some JavaScript runtimes do not yet support this,
|
||||
which includes versions of Node.js prior to 20.19.0.
|
||||
|
||||
This package converts the `color` package and its dependencies,
|
||||
all of which are MIT-licensed, to CommonJS.
|
||||
|
||||
- [color](https://www.npmjs.com/package/color)
|
||||
- [color-convert](https://www.npmjs.com/package/color-convert)
|
||||
- [color-string](https://www.npmjs.com/package/color-string)
|
||||
- [color-name](https://www.npmjs.com/package/color-name)
|
||||
+1596
File diff suppressed because it is too large
Load Diff
+1
@@ -0,0 +1 @@
|
||||
module.exports = require("./color.cjs").default;
|
||||
+929
@@ -0,0 +1,929 @@
|
||||
// Generated by dts-bundle-generator v9.5.1
|
||||
|
||||
type Channels = number;
|
||||
type RGB = [
|
||||
r: number,
|
||||
g: number,
|
||||
b: number
|
||||
];
|
||||
type HSL = [
|
||||
h: number,
|
||||
s: number,
|
||||
l: number
|
||||
];
|
||||
type HSV = [
|
||||
h: number,
|
||||
s: number,
|
||||
v: number
|
||||
];
|
||||
type CMYK = [
|
||||
c: number,
|
||||
m: number,
|
||||
y: number,
|
||||
k: number
|
||||
];
|
||||
type LAB = [
|
||||
l: number,
|
||||
a: number,
|
||||
b: number
|
||||
];
|
||||
type LCH = [
|
||||
l: number,
|
||||
c: number,
|
||||
h: number
|
||||
];
|
||||
type HCG = [
|
||||
h: number,
|
||||
c: number,
|
||||
g: number
|
||||
];
|
||||
type HWB = [
|
||||
h: number,
|
||||
w: number,
|
||||
b: number
|
||||
];
|
||||
type XYZ = [
|
||||
x: number,
|
||||
y: number,
|
||||
z: number
|
||||
];
|
||||
type Apple = [
|
||||
r16: number,
|
||||
g16: number,
|
||||
b16: number
|
||||
];
|
||||
type Gray = [
|
||||
gray: number
|
||||
];
|
||||
type ANSI16 = number;
|
||||
type ANSI256 = number;
|
||||
type Keyword = string;
|
||||
type HEX = string;
|
||||
declare namespace route {
|
||||
type rgb = {
|
||||
hsl(from: RGB): HSL;
|
||||
hsl(...from: RGB): HSL;
|
||||
hsl(from: RGB): HSL;
|
||||
hsl(...from: RGB): HSL;
|
||||
hsv(from: RGB): HSV;
|
||||
hsv(...from: RGB): HSV;
|
||||
hwb(from: RGB): HWB;
|
||||
hwb(...from: RGB): HWB;
|
||||
cmyk(from: RGB): CMYK;
|
||||
cmyk(...from: RGB): CMYK;
|
||||
xyz(from: RGB): XYZ;
|
||||
xyz(...from: RGB): XYZ;
|
||||
lab(from: RGB): LAB;
|
||||
lab(...from: RGB): LAB;
|
||||
lch(from: RGB): LCH;
|
||||
lch(...from: RGB): LCH;
|
||||
hex(from: RGB): HEX;
|
||||
hex(...from: RGB): HEX;
|
||||
keyword(from: RGB): Keyword;
|
||||
keyword(...from: RGB): Keyword;
|
||||
ansi16(from: RGB): ANSI16;
|
||||
ansi16(...from: RGB): ANSI16;
|
||||
ansi256(from: RGB): ANSI256;
|
||||
ansi256(...from: RGB): ANSI256;
|
||||
hcg(from: RGB): HCG;
|
||||
hcg(...from: RGB): HCG;
|
||||
apple(from: RGB): Apple;
|
||||
apple(...from: RGB): Apple;
|
||||
gray(from: RGB): Gray;
|
||||
gray(...from: RGB): Gray;
|
||||
};
|
||||
type hsl = {
|
||||
rgb(from: HSL): RGB;
|
||||
rgb(...from: HSL): RGB;
|
||||
hsv(from: HSL): HSV;
|
||||
hsv(...from: HSL): HSV;
|
||||
hwb(from: HSL): HWB;
|
||||
hwb(...from: HSL): HWB;
|
||||
cmyk(from: HSL): CMYK;
|
||||
cmyk(...from: HSL): CMYK;
|
||||
xyz(from: HSL): XYZ;
|
||||
xyz(...from: HSL): XYZ;
|
||||
lab(from: HSL): LAB;
|
||||
lab(...from: HSL): LAB;
|
||||
lch(from: HSL): LCH;
|
||||
lch(...from: HSL): LCH;
|
||||
hex(from: HSL): HEX;
|
||||
hex(...from: HSL): HEX;
|
||||
keyword(from: HSL): Keyword;
|
||||
keyword(...from: HSL): Keyword;
|
||||
ansi16(from: HSL): ANSI16;
|
||||
ansi16(...from: HSL): ANSI16;
|
||||
ansi256(from: HSL): ANSI256;
|
||||
ansi256(...from: HSL): ANSI256;
|
||||
hcg(from: HSL): HCG;
|
||||
hcg(...from: HSL): HCG;
|
||||
apple(from: HSL): Apple;
|
||||
apple(...from: HSL): Apple;
|
||||
gray(from: HSL): Gray;
|
||||
gray(...from: HSL): Gray;
|
||||
};
|
||||
type hsv = {
|
||||
rgb(from: HSV): RGB;
|
||||
rgb(...from: HSV): RGB;
|
||||
hsl(from: HSV): HSL;
|
||||
hsl(...from: HSV): HSL;
|
||||
hwb(from: HSV): HWB;
|
||||
hwb(...from: HSV): HWB;
|
||||
cmyk(from: HSV): CMYK;
|
||||
cmyk(...from: HSV): CMYK;
|
||||
xyz(from: HSV): XYZ;
|
||||
xyz(...from: HSV): XYZ;
|
||||
lab(from: HSV): LAB;
|
||||
lab(...from: HSV): LAB;
|
||||
lch(from: HSV): LCH;
|
||||
lch(...from: HSV): LCH;
|
||||
hex(from: HSV): HEX;
|
||||
hex(...from: HSV): HEX;
|
||||
keyword(from: HSV): Keyword;
|
||||
keyword(...from: HSV): Keyword;
|
||||
ansi16(from: HSV): ANSI16;
|
||||
ansi16(...from: HSV): ANSI16;
|
||||
ansi256(from: HSV): ANSI256;
|
||||
ansi256(...from: HSV): ANSI256;
|
||||
hcg(from: HSV): HCG;
|
||||
hcg(...from: HSV): HCG;
|
||||
apple(from: HSV): Apple;
|
||||
apple(...from: HSV): Apple;
|
||||
gray(from: HSV): Gray;
|
||||
gray(...from: HSV): Gray;
|
||||
};
|
||||
type hwb = {
|
||||
rgb(from: HWB): RGB;
|
||||
rgb(...from: HWB): RGB;
|
||||
hsl(from: HWB): HSL;
|
||||
hsl(...from: HWB): HSL;
|
||||
hsv(from: HWB): HSV;
|
||||
hsv(...from: HWB): HSV;
|
||||
cmyk(from: HWB): CMYK;
|
||||
cmyk(...from: HWB): CMYK;
|
||||
xyz(from: HWB): XYZ;
|
||||
xyz(...from: HWB): XYZ;
|
||||
lab(from: HWB): LAB;
|
||||
lab(...from: HWB): LAB;
|
||||
lch(from: HWB): LCH;
|
||||
lch(...from: HWB): LCH;
|
||||
hex(from: HWB): HEX;
|
||||
hex(...from: HWB): HEX;
|
||||
keyword(from: HWB): Keyword;
|
||||
keyword(...from: HWB): Keyword;
|
||||
ansi16(from: HWB): ANSI16;
|
||||
ansi16(...from: HWB): ANSI16;
|
||||
ansi256(from: HWB): ANSI256;
|
||||
ansi256(...from: HWB): ANSI256;
|
||||
hcg(from: HWB): HCG;
|
||||
hcg(...from: HWB): HCG;
|
||||
apple(from: HWB): Apple;
|
||||
apple(...from: HWB): Apple;
|
||||
gray(from: HWB): Gray;
|
||||
gray(...from: HWB): Gray;
|
||||
};
|
||||
type cmyk = {
|
||||
rgb(from: CMYK): RGB;
|
||||
rgb(...from: CMYK): RGB;
|
||||
hsl(from: CMYK): HSL;
|
||||
hsl(...from: CMYK): HSL;
|
||||
hsv(from: CMYK): HSV;
|
||||
hsv(...from: CMYK): HSV;
|
||||
hwb(from: CMYK): HWB;
|
||||
hwb(...from: CMYK): HWB;
|
||||
xyz(from: CMYK): XYZ;
|
||||
xyz(...from: CMYK): XYZ;
|
||||
lab(from: CMYK): LAB;
|
||||
lab(...from: CMYK): LAB;
|
||||
lch(from: CMYK): LCH;
|
||||
lch(...from: CMYK): LCH;
|
||||
hex(from: CMYK): HEX;
|
||||
hex(...from: CMYK): HEX;
|
||||
keyword(from: CMYK): Keyword;
|
||||
keyword(...from: CMYK): Keyword;
|
||||
ansi16(from: CMYK): ANSI16;
|
||||
ansi16(...from: CMYK): ANSI16;
|
||||
ansi256(from: CMYK): ANSI256;
|
||||
ansi256(...from: CMYK): ANSI256;
|
||||
hcg(from: CMYK): HCG;
|
||||
hcg(...from: CMYK): HCG;
|
||||
apple(from: CMYK): Apple;
|
||||
apple(...from: CMYK): Apple;
|
||||
gray(from: CMYK): Gray;
|
||||
gray(...from: CMYK): Gray;
|
||||
};
|
||||
type xyz = {
|
||||
rgb(from: XYZ): RGB;
|
||||
rgb(...from: XYZ): RGB;
|
||||
hsl(from: XYZ): HSL;
|
||||
hsl(...from: XYZ): HSL;
|
||||
hsv(from: XYZ): HSV;
|
||||
hsv(...from: XYZ): HSV;
|
||||
hwb(from: XYZ): HWB;
|
||||
hwb(...from: XYZ): HWB;
|
||||
cmyk(from: XYZ): CMYK;
|
||||
cmyk(...from: XYZ): CMYK;
|
||||
lab(from: XYZ): LAB;
|
||||
lab(...from: XYZ): LAB;
|
||||
lch(from: XYZ): LCH;
|
||||
lch(...from: XYZ): LCH;
|
||||
hex(from: XYZ): HEX;
|
||||
hex(...from: XYZ): HEX;
|
||||
keyword(from: XYZ): Keyword;
|
||||
keyword(...from: XYZ): Keyword;
|
||||
ansi16(from: XYZ): ANSI16;
|
||||
ansi16(...from: XYZ): ANSI16;
|
||||
ansi256(from: XYZ): ANSI256;
|
||||
ansi256(...from: XYZ): ANSI256;
|
||||
hcg(from: XYZ): HCG;
|
||||
hcg(...from: XYZ): HCG;
|
||||
apple(from: XYZ): Apple;
|
||||
apple(...from: XYZ): Apple;
|
||||
gray(from: XYZ): Gray;
|
||||
gray(...from: XYZ): Gray;
|
||||
};
|
||||
type lab = {
|
||||
rgb(from: LAB): RGB;
|
||||
rgb(...from: LAB): RGB;
|
||||
hsl(from: LAB): HSL;
|
||||
hsl(...from: LAB): HSL;
|
||||
hsv(from: LAB): HSV;
|
||||
hsv(...from: LAB): HSV;
|
||||
hwb(from: LAB): HWB;
|
||||
hwb(...from: LAB): HWB;
|
||||
cmyk(from: LAB): CMYK;
|
||||
cmyk(...from: LAB): CMYK;
|
||||
xyz(from: LAB): XYZ;
|
||||
xyz(...from: LAB): XYZ;
|
||||
lch(from: LAB): LCH;
|
||||
lch(...from: LAB): LCH;
|
||||
hex(from: LAB): HEX;
|
||||
hex(...from: LAB): HEX;
|
||||
keyword(from: LAB): Keyword;
|
||||
keyword(...from: LAB): Keyword;
|
||||
ansi16(from: LAB): ANSI16;
|
||||
ansi16(...from: LAB): ANSI16;
|
||||
ansi256(from: LAB): ANSI256;
|
||||
ansi256(...from: LAB): ANSI256;
|
||||
hcg(from: LAB): HCG;
|
||||
hcg(...from: LAB): HCG;
|
||||
apple(from: LAB): Apple;
|
||||
apple(...from: LAB): Apple;
|
||||
gray(from: LAB): Gray;
|
||||
gray(...from: LAB): Gray;
|
||||
};
|
||||
type lch = {
|
||||
rgb(from: LCH): RGB;
|
||||
rgb(...from: LCH): RGB;
|
||||
hsl(from: LCH): HSL;
|
||||
hsl(...from: LCH): HSL;
|
||||
hsv(from: LCH): HSV;
|
||||
hsv(...from: LCH): HSV;
|
||||
hwb(from: LCH): HWB;
|
||||
hwb(...from: LCH): HWB;
|
||||
cmyk(from: LCH): CMYK;
|
||||
cmyk(...from: LCH): CMYK;
|
||||
xyz(from: LCH): XYZ;
|
||||
xyz(...from: LCH): XYZ;
|
||||
lab(from: LCH): LAB;
|
||||
lab(...from: LCH): LAB;
|
||||
hex(from: LCH): HEX;
|
||||
hex(...from: LCH): HEX;
|
||||
keyword(from: LCH): Keyword;
|
||||
keyword(...from: LCH): Keyword;
|
||||
ansi16(from: LCH): ANSI16;
|
||||
ansi16(...from: LCH): ANSI16;
|
||||
ansi256(from: LCH): ANSI256;
|
||||
ansi256(...from: LCH): ANSI256;
|
||||
hcg(from: LCH): HCG;
|
||||
hcg(...from: LCH): HCG;
|
||||
apple(from: LCH): Apple;
|
||||
apple(...from: LCH): Apple;
|
||||
gray(from: LCH): Gray;
|
||||
gray(...from: LCH): Gray;
|
||||
};
|
||||
type hex = {
|
||||
rgb(from: HEX): RGB;
|
||||
hsl(from: HEX): HSL;
|
||||
hsv(from: HEX): HSV;
|
||||
hwb(from: HEX): HWB;
|
||||
cmyk(from: HEX): CMYK;
|
||||
xyz(from: HEX): XYZ;
|
||||
lab(from: HEX): LAB;
|
||||
lch(from: HEX): LCH;
|
||||
keyword(from: HEX): Keyword;
|
||||
ansi16(from: HEX): ANSI16;
|
||||
ansi256(from: HEX): ANSI256;
|
||||
hcg(from: HEX): HCG;
|
||||
apple(from: HEX): Apple;
|
||||
gray(from: HEX): Gray;
|
||||
};
|
||||
type keyword = {
|
||||
rgb(from: Keyword): RGB;
|
||||
hsl(from: Keyword): HSL;
|
||||
hsv(from: Keyword): HSV;
|
||||
hwb(from: Keyword): HWB;
|
||||
cmyk(from: Keyword): CMYK;
|
||||
xyz(from: Keyword): XYZ;
|
||||
lab(from: Keyword): LAB;
|
||||
lch(from: Keyword): LCH;
|
||||
hex(from: Keyword): HEX;
|
||||
ansi16(from: Keyword): ANSI16;
|
||||
ansi256(from: Keyword): ANSI256;
|
||||
hcg(from: Keyword): HCG;
|
||||
apple(from: Keyword): Apple;
|
||||
gray(from: Keyword): Gray;
|
||||
};
|
||||
type ansi16 = {
|
||||
rgb(from: ANSI16): RGB;
|
||||
hsl(from: ANSI16): HSL;
|
||||
hsv(from: ANSI16): HSV;
|
||||
hwb(from: ANSI16): HWB;
|
||||
cmyk(from: ANSI16): CMYK;
|
||||
xyz(from: ANSI16): XYZ;
|
||||
lab(from: ANSI16): LAB;
|
||||
lch(from: ANSI16): LCH;
|
||||
hex(from: ANSI16): HEX;
|
||||
keyword(from: ANSI16): Keyword;
|
||||
ansi256(from: ANSI16): ANSI256;
|
||||
hcg(from: ANSI16): HCG;
|
||||
apple(from: ANSI16): Apple;
|
||||
gray(from: ANSI16): Gray;
|
||||
};
|
||||
type ansi256 = {
|
||||
rgb(from: ANSI256): RGB;
|
||||
hsl(from: ANSI256): HSL;
|
||||
hsv(from: ANSI256): HSV;
|
||||
hwb(from: ANSI256): HWB;
|
||||
cmyk(from: ANSI256): CMYK;
|
||||
xyz(from: ANSI256): XYZ;
|
||||
lab(from: ANSI256): LAB;
|
||||
lch(from: ANSI256): LCH;
|
||||
hex(from: ANSI256): HEX;
|
||||
keyword(from: ANSI256): Keyword;
|
||||
ansi16(from: ANSI256): ANSI16;
|
||||
hcg(from: ANSI256): HCG;
|
||||
apple(from: ANSI256): Apple;
|
||||
gray(from: ANSI256): Gray;
|
||||
};
|
||||
type hcg = {
|
||||
rgb(from: HCG): RGB;
|
||||
rgb(...from: HCG): RGB;
|
||||
hsl(from: HCG): HSL;
|
||||
hsl(...from: HCG): HSL;
|
||||
hsv(from: HCG): HSV;
|
||||
hsv(...from: HCG): HSV;
|
||||
hwb(from: HCG): HWB;
|
||||
hwb(...from: HCG): HWB;
|
||||
cmyk(from: HCG): CMYK;
|
||||
cmyk(...from: HCG): CMYK;
|
||||
xyz(from: HCG): XYZ;
|
||||
xyz(...from: HCG): XYZ;
|
||||
lab(from: HCG): LAB;
|
||||
lab(...from: HCG): LAB;
|
||||
lch(from: HCG): LCH;
|
||||
lch(...from: HCG): LCH;
|
||||
hex(from: HCG): HEX;
|
||||
hex(...from: HCG): HEX;
|
||||
keyword(from: HCG): Keyword;
|
||||
keyword(...from: HCG): Keyword;
|
||||
ansi16(from: HCG): ANSI16;
|
||||
ansi16(...from: HCG): ANSI16;
|
||||
ansi256(from: HCG): ANSI256;
|
||||
ansi256(...from: HCG): ANSI256;
|
||||
apple(from: HCG): Apple;
|
||||
apple(...from: HCG): Apple;
|
||||
gray(from: HCG): Gray;
|
||||
gray(...from: HCG): Gray;
|
||||
};
|
||||
type apple = {
|
||||
rgb(from: Apple): RGB;
|
||||
rgb(...from: Apple): RGB;
|
||||
hsl(from: Apple): HSL;
|
||||
hsl(...from: Apple): HSL;
|
||||
hsv(from: Apple): HSV;
|
||||
hsv(...from: Apple): HSV;
|
||||
hwb(from: Apple): HWB;
|
||||
hwb(...from: Apple): HWB;
|
||||
cmyk(from: Apple): CMYK;
|
||||
cmyk(...from: Apple): CMYK;
|
||||
xyz(from: Apple): XYZ;
|
||||
xyz(...from: Apple): XYZ;
|
||||
lab(from: Apple): LAB;
|
||||
lab(...from: Apple): LAB;
|
||||
lch(from: Apple): LCH;
|
||||
lch(...from: Apple): LCH;
|
||||
hex(from: Apple): HEX;
|
||||
hex(...from: Apple): HEX;
|
||||
keyword(from: Apple): Keyword;
|
||||
keyword(...from: Apple): Keyword;
|
||||
ansi16(from: Apple): ANSI16;
|
||||
ansi16(...from: Apple): ANSI16;
|
||||
ansi256(from: Apple): ANSI256;
|
||||
ansi256(...from: Apple): ANSI256;
|
||||
hcg(from: Apple): HCG;
|
||||
hcg(...from: Apple): HCG;
|
||||
gray(from: Apple): Gray;
|
||||
gray(...from: Apple): Gray;
|
||||
};
|
||||
type gray = {
|
||||
rgb(from: Gray): RGB;
|
||||
rgb(...from: Gray): RGB;
|
||||
hsl(from: Gray): HSL;
|
||||
hsl(...from: Gray): HSL;
|
||||
hsv(from: Gray): HSV;
|
||||
hsv(...from: Gray): HSV;
|
||||
hwb(from: Gray): HWB;
|
||||
hwb(...from: Gray): HWB;
|
||||
cmyk(from: Gray): CMYK;
|
||||
cmyk(...from: Gray): CMYK;
|
||||
xyz(from: Gray): XYZ;
|
||||
xyz(...from: Gray): XYZ;
|
||||
lab(from: Gray): LAB;
|
||||
lab(...from: Gray): LAB;
|
||||
lch(from: Gray): LCH;
|
||||
lch(...from: Gray): LCH;
|
||||
hex(from: Gray): HEX;
|
||||
hex(...from: Gray): HEX;
|
||||
keyword(from: Gray): Keyword;
|
||||
keyword(...from: Gray): Keyword;
|
||||
ansi16(from: Gray): ANSI16;
|
||||
ansi16(...from: Gray): ANSI16;
|
||||
ansi256(from: Gray): ANSI256;
|
||||
ansi256(...from: Gray): ANSI256;
|
||||
hcg(from: Gray): HCG;
|
||||
hcg(...from: Gray): HCG;
|
||||
apple(from: Gray): Apple;
|
||||
apple(...from: Gray): Apple;
|
||||
};
|
||||
}
|
||||
declare function route(fromModel: "rgb"): route.rgb;
|
||||
declare function route(fromModel: "hsl"): route.hsl;
|
||||
declare function route(fromModel: "hsv"): route.hsv;
|
||||
declare function route(fromModel: "hwb"): route.hwb;
|
||||
declare function route(fromModel: "cmyk"): route.cmyk;
|
||||
declare function route(fromModel: "xyz"): route.xyz;
|
||||
declare function route(fromModel: "lab"): route.lab;
|
||||
declare function route(fromModel: "lch"): route.lch;
|
||||
declare function route(fromModel: "hex"): route.hex;
|
||||
declare function route(fromModel: "keyword"): route.keyword;
|
||||
declare function route(fromModel: "ansi16"): route.ansi16;
|
||||
declare function route(fromModel: "ansi256"): route.ansi256;
|
||||
declare function route(fromModel: "hcg"): route.hcg;
|
||||
declare function route(fromModel: "apple"): route.apple;
|
||||
declare function route(fromModel: "gray"): route.gray;
|
||||
type Convert = {
|
||||
rgb: {
|
||||
channels: Channels;
|
||||
labels: "rgb";
|
||||
hsl: {
|
||||
(...rgb: RGB): HSL;
|
||||
raw: (...rgb: RGB) => HSL;
|
||||
};
|
||||
hsv: {
|
||||
(...rgb: RGB): HSV;
|
||||
raw: (...rgb: RGB) => HSV;
|
||||
};
|
||||
hwb: {
|
||||
(...rgb: RGB): HWB;
|
||||
raw: (...rgb: RGB) => HWB;
|
||||
};
|
||||
hcg: {
|
||||
(...rgb: RGB): HCG;
|
||||
raw: (...rgb: RGB) => HCG;
|
||||
};
|
||||
cmyk: {
|
||||
(...rgb: RGB): CMYK;
|
||||
raw: (...rgb: RGB) => CMYK;
|
||||
};
|
||||
keyword: {
|
||||
(...rgb: RGB): Keyword;
|
||||
raw: (...rgb: RGB) => Keyword;
|
||||
};
|
||||
ansi16: {
|
||||
(...rgb: RGB): ANSI16;
|
||||
raw: (...rgb: RGB) => ANSI16;
|
||||
};
|
||||
ansi256: {
|
||||
(...rgb: RGB): ANSI256;
|
||||
raw: (...rgb: RGB) => ANSI256;
|
||||
};
|
||||
apple: {
|
||||
(...rgb: RGB): Apple;
|
||||
raw: (...rgb: RGB) => Apple;
|
||||
};
|
||||
hex: {
|
||||
(...rgb: RGB): HEX;
|
||||
raw: (...rgb: RGB) => HEX;
|
||||
};
|
||||
gray: {
|
||||
(...rgb: RGB): Gray;
|
||||
raw: (...rgb: RGB) => Gray;
|
||||
};
|
||||
} & route.rgb & {
|
||||
[F in keyof route.rgb]: {
|
||||
raw: route.rgb[F];
|
||||
};
|
||||
};
|
||||
keyword: {
|
||||
channels: Channels;
|
||||
rgb: {
|
||||
(keyword: Keyword): RGB;
|
||||
raw: (keyword: Keyword) => RGB;
|
||||
};
|
||||
} & route.keyword & {
|
||||
[F in keyof route.keyword]: {
|
||||
raw: route.keyword[F];
|
||||
};
|
||||
};
|
||||
hsl: {
|
||||
channels: Channels;
|
||||
labels: "hsl";
|
||||
rgb: {
|
||||
(...hsl: HSL): RGB;
|
||||
raw: (...hsl: HSL) => RGB;
|
||||
};
|
||||
hsv: {
|
||||
(...hsl: HSL): HSV;
|
||||
raw: (...hsl: HSL) => HSV;
|
||||
};
|
||||
hcg: {
|
||||
(...hsl: HSL): HCG;
|
||||
raw: (...hsl: HSL) => HCG;
|
||||
};
|
||||
} & route.hsl & {
|
||||
[F in keyof route.hsl]: {
|
||||
raw: route.hsl[F];
|
||||
};
|
||||
};
|
||||
hsv: {
|
||||
channels: Channels;
|
||||
labels: "hsv";
|
||||
hcg: {
|
||||
(...hsv: HSV): HCG;
|
||||
raw: (...hsv: HSV) => HCG;
|
||||
};
|
||||
rgb: {
|
||||
(...hsv: HSV): RGB;
|
||||
raw: (...hsv: HSV) => RGB;
|
||||
};
|
||||
hsv: {
|
||||
(...hsv: HSV): HSV;
|
||||
raw: (...hsv: HSV) => HSV;
|
||||
};
|
||||
hsl: {
|
||||
(...hsv: HSV): HSL;
|
||||
raw: (...hsv: HSV) => HSL;
|
||||
};
|
||||
hwb: {
|
||||
(...hsv: HSV): HWB;
|
||||
raw: (...hsv: HSV) => HWB;
|
||||
};
|
||||
ansi16: {
|
||||
(...hsv: HSV): ANSI16;
|
||||
raw: (...hsv: HSV) => ANSI16;
|
||||
};
|
||||
} & route.hsv & {
|
||||
[F in keyof route.hsv]: {
|
||||
raw: route.hsv[F];
|
||||
};
|
||||
};
|
||||
hwb: {
|
||||
channels: Channels;
|
||||
labels: "hwb";
|
||||
hcg: {
|
||||
(...hwb: HWB): HCG;
|
||||
raw: (...hwb: HWB) => HCG;
|
||||
};
|
||||
rgb: {
|
||||
(...hwb: HWB): RGB;
|
||||
raw: (...hwb: HWB) => RGB;
|
||||
};
|
||||
} & route.hwb & {
|
||||
[F in keyof route.hwb]: {
|
||||
raw: route.hwb[F];
|
||||
};
|
||||
};
|
||||
cmyk: {
|
||||
channels: Channels;
|
||||
labels: "cmyk";
|
||||
rgb: {
|
||||
(...cmyk: CMYK): RGB;
|
||||
raw: (...cmyk: CMYK) => RGB;
|
||||
};
|
||||
} & route.cmyk & {
|
||||
[F in keyof route.cmyk]: {
|
||||
raw: route.cmyk[F];
|
||||
};
|
||||
};
|
||||
xyz: {
|
||||
channels: Channels;
|
||||
labels: "xyz";
|
||||
rgb: {
|
||||
(...xyz: XYZ): RGB;
|
||||
raw: (...xyz: XYZ) => RGB;
|
||||
};
|
||||
lab: {
|
||||
(...xyz: XYZ): LAB;
|
||||
raw: (...xyz: XYZ) => LAB;
|
||||
};
|
||||
} & route.xyz & {
|
||||
[F in keyof route.xyz]: {
|
||||
raw: route.xyz[F];
|
||||
};
|
||||
};
|
||||
lab: {
|
||||
channels: Channels;
|
||||
labels: "lab";
|
||||
xyz: {
|
||||
(...lab: LAB): XYZ;
|
||||
raw: (...lab: LAB) => XYZ;
|
||||
};
|
||||
lch: {
|
||||
(...lab: LAB): LCH;
|
||||
raw: (...lab: LAB) => LCH;
|
||||
};
|
||||
} & route.lab & {
|
||||
[F in keyof route.lab]: {
|
||||
raw: route.lab[F];
|
||||
};
|
||||
};
|
||||
lch: {
|
||||
channels: Channels;
|
||||
labels: "lch";
|
||||
lab: {
|
||||
(...lch: LCH): LAB;
|
||||
raw: (...lch: LCH) => LAB;
|
||||
};
|
||||
} & route.lch & {
|
||||
[F in keyof route.lch]: {
|
||||
raw: route.lch[F];
|
||||
};
|
||||
};
|
||||
hex: {
|
||||
channels: Channels;
|
||||
labels: [
|
||||
"hex"
|
||||
];
|
||||
rgb: {
|
||||
(hex: HEX): RGB;
|
||||
raw: (hex: HEX) => RGB;
|
||||
};
|
||||
} & route.hex & {
|
||||
[F in keyof route.hex]: {
|
||||
raw: route.hex[F];
|
||||
};
|
||||
};
|
||||
ansi16: {
|
||||
channels: Channels;
|
||||
labels: [
|
||||
"ansi16"
|
||||
];
|
||||
rgb: {
|
||||
(ansi16: ANSI16): RGB;
|
||||
raw: (ansi16: ANSI16) => RGB;
|
||||
};
|
||||
} & route.ansi16 & {
|
||||
[F in keyof route.ansi16]: {
|
||||
raw: route.ansi16[F];
|
||||
};
|
||||
};
|
||||
ansi256: {
|
||||
channels: Channels;
|
||||
labels: [
|
||||
"ansi256"
|
||||
];
|
||||
rgb: {
|
||||
(ansi256: ANSI256): RGB;
|
||||
raw: (ansi256: ANSI256) => RGB;
|
||||
};
|
||||
} & route.ansi256 & {
|
||||
[F in keyof route.ansi256]: {
|
||||
raw: route.ansi256[F];
|
||||
};
|
||||
};
|
||||
hcg: {
|
||||
channels: Channels;
|
||||
labels: [
|
||||
"h",
|
||||
"c",
|
||||
"g"
|
||||
];
|
||||
rgb: {
|
||||
(...hcg: HCG): RGB;
|
||||
raw: (...hcg: HCG) => RGB;
|
||||
};
|
||||
hsv: {
|
||||
(...hcg: HCG): HSV;
|
||||
raw: (...hcg: HCG) => HSV;
|
||||
};
|
||||
hwb: {
|
||||
(...hcg: HCG): HWB;
|
||||
raw: (...hcg: HCG) => HWB;
|
||||
};
|
||||
} & route.hcg & {
|
||||
[F in keyof route.hcg]: {
|
||||
raw: route.hcg[F];
|
||||
};
|
||||
};
|
||||
apple: {
|
||||
channels: Channels;
|
||||
labels: [
|
||||
"r16",
|
||||
"g16",
|
||||
"b16"
|
||||
];
|
||||
rgb: {
|
||||
(...apple: Apple): RGB;
|
||||
raw: (...apple: Apple) => RGB;
|
||||
};
|
||||
} & route.apple & {
|
||||
[F in keyof route.apple]: {
|
||||
raw: route.apple[F];
|
||||
};
|
||||
};
|
||||
gray: {
|
||||
channels: Channels;
|
||||
labels: [
|
||||
"gray"
|
||||
];
|
||||
rgb: {
|
||||
(...gray: Gray): RGB;
|
||||
raw: (...gray: Gray) => RGB;
|
||||
};
|
||||
hsl: {
|
||||
(...gray: Gray): HSL;
|
||||
raw: (...gray: Gray) => HSL;
|
||||
};
|
||||
hsv: {
|
||||
(...gray: Gray): HSV;
|
||||
raw: (...gray: Gray) => HSV;
|
||||
};
|
||||
hwb: {
|
||||
(...gray: Gray): HWB;
|
||||
raw: (...gray: Gray) => HWB;
|
||||
};
|
||||
cmyk: {
|
||||
(...gray: Gray): CMYK;
|
||||
raw: (...gray: Gray) => CMYK;
|
||||
};
|
||||
lab: {
|
||||
(...gray: Gray): LAB;
|
||||
raw: (...gray: Gray) => LAB;
|
||||
};
|
||||
hex: {
|
||||
(...gray: Gray): HEX;
|
||||
raw: (...gray: Gray) => HEX;
|
||||
};
|
||||
} & route.gray & {
|
||||
[F in keyof route.gray]: {
|
||||
raw: route.gray[F];
|
||||
};
|
||||
};
|
||||
};
|
||||
declare const convert: Convert;
|
||||
export type ColorLike = ColorInstance | string | ArrayLike<number> | number | Record<string, any>;
|
||||
export type ColorJson = {
|
||||
model: string;
|
||||
color: number[];
|
||||
valpha: number;
|
||||
};
|
||||
export type ColorObject = {
|
||||
alpha?: number | undefined;
|
||||
} & Record<string, number>;
|
||||
// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
|
||||
export interface ColorInstance {
|
||||
toString(): string;
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
toJSON(): ColorJson;
|
||||
string(places?: number): string;
|
||||
percentString(places?: number): string;
|
||||
array(): number[];
|
||||
object(): ColorObject;
|
||||
unitArray(): number[];
|
||||
unitObject(): {
|
||||
r: number;
|
||||
g: number;
|
||||
b: number;
|
||||
alpha?: number | undefined;
|
||||
};
|
||||
round(places?: number): ColorInstance;
|
||||
alpha(): number;
|
||||
alpha(value: number): ColorInstance;
|
||||
red(): number;
|
||||
red(value: number): ColorInstance;
|
||||
green(): number;
|
||||
green(value: number): ColorInstance;
|
||||
blue(): number;
|
||||
blue(value: number): ColorInstance;
|
||||
hue(): number;
|
||||
hue(value: number): ColorInstance;
|
||||
saturationl(): number;
|
||||
saturationl(value: number): ColorInstance;
|
||||
lightness(): number;
|
||||
lightness(value: number): ColorInstance;
|
||||
saturationv(): number;
|
||||
saturationv(value: number): ColorInstance;
|
||||
value(): number;
|
||||
value(value: number): ColorInstance;
|
||||
chroma(): number;
|
||||
chroma(value: number): ColorInstance;
|
||||
gray(): number;
|
||||
gray(value: number): ColorInstance;
|
||||
white(): number;
|
||||
white(value: number): ColorInstance;
|
||||
wblack(): number;
|
||||
wblack(value: number): ColorInstance;
|
||||
cyan(): number;
|
||||
cyan(value: number): ColorInstance;
|
||||
magenta(): number;
|
||||
magenta(value: number): ColorInstance;
|
||||
yellow(): number;
|
||||
yellow(value: number): ColorInstance;
|
||||
black(): number;
|
||||
black(value: number): ColorInstance;
|
||||
x(): number;
|
||||
x(value: number): ColorInstance;
|
||||
y(): number;
|
||||
y(value: number): ColorInstance;
|
||||
z(): number;
|
||||
z(value: number): ColorInstance;
|
||||
l(): number;
|
||||
l(value: number): ColorInstance;
|
||||
a(): number;
|
||||
a(value: number): ColorInstance;
|
||||
b(): number;
|
||||
b(value: number): ColorInstance;
|
||||
keyword(): string;
|
||||
keyword<V extends string>(value: V): ColorInstance;
|
||||
hex(): string;
|
||||
hex<V extends string>(value: V): ColorInstance;
|
||||
hexa(): string;
|
||||
hexa<V extends string>(value: V): ColorInstance;
|
||||
rgbNumber(): number;
|
||||
luminosity(): number;
|
||||
contrast(color2: ColorInstance): number;
|
||||
level(color2: ColorInstance): "AAA" | "AA" | "";
|
||||
isDark(): boolean;
|
||||
isLight(): boolean;
|
||||
negate(): ColorInstance;
|
||||
lighten(ratio: number): ColorInstance;
|
||||
darken(ratio: number): ColorInstance;
|
||||
saturate(ratio: number): ColorInstance;
|
||||
desaturate(ratio: number): ColorInstance;
|
||||
whiten(ratio: number): ColorInstance;
|
||||
blacken(ratio: number): ColorInstance;
|
||||
grayscale(): ColorInstance;
|
||||
fade(ratio: number): ColorInstance;
|
||||
opaquer(ratio: number): ColorInstance;
|
||||
rotate(degrees: number): ColorInstance;
|
||||
mix(mixinColor: ColorInstance, weight?: number): ColorInstance;
|
||||
rgb(...arguments_: number[]): ColorInstance;
|
||||
hsl(...arguments_: number[]): ColorInstance;
|
||||
hsv(...arguments_: number[]): ColorInstance;
|
||||
hwb(...arguments_: number[]): ColorInstance;
|
||||
cmyk(...arguments_: number[]): ColorInstance;
|
||||
xyz(...arguments_: number[]): ColorInstance;
|
||||
lab(...arguments_: number[]): ColorInstance;
|
||||
lch(...arguments_: number[]): ColorInstance;
|
||||
ansi16(...arguments_: number[]): ColorInstance;
|
||||
ansi256(...arguments_: number[]): ColorInstance;
|
||||
hcg(...arguments_: number[]): ColorInstance;
|
||||
apple(...arguments_: number[]): ColorInstance;
|
||||
}
|
||||
export type ColorConstructor = {
|
||||
(object?: ColorLike, model?: keyof (typeof convert)): ColorInstance;
|
||||
new (object?: ColorLike, model?: keyof (typeof convert)): ColorInstance;
|
||||
rgb(...value: number[]): ColorInstance;
|
||||
rgb(color: ColorLike): ColorInstance;
|
||||
hsl(...value: number[]): ColorInstance;
|
||||
hsl(color: ColorLike): ColorInstance;
|
||||
hsv(...value: number[]): ColorInstance;
|
||||
hsv(color: ColorLike): ColorInstance;
|
||||
hwb(...value: number[]): ColorInstance;
|
||||
hwb(color: ColorLike): ColorInstance;
|
||||
cmyk(...value: number[]): ColorInstance;
|
||||
cmyk(color: ColorLike): ColorInstance;
|
||||
xyz(...value: number[]): ColorInstance;
|
||||
xyz(color: ColorLike): ColorInstance;
|
||||
lab(...value: number[]): ColorInstance;
|
||||
lab(color: ColorLike): ColorInstance;
|
||||
lch(...value: number[]): ColorInstance;
|
||||
lch(color: ColorLike): ColorInstance;
|
||||
ansi16(...value: number[]): ColorInstance;
|
||||
ansi16(color: ColorLike): ColorInstance;
|
||||
ansi256(...value: number[]): ColorInstance;
|
||||
ansi256(color: ColorLike): ColorInstance;
|
||||
hcg(...value: number[]): ColorInstance;
|
||||
hcg(color: ColorLike): ColorInstance;
|
||||
apple(...value: number[]): ColorInstance;
|
||||
apple(color: ColorLike): ColorInstance;
|
||||
};
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
declare const Color: ColorConstructor;
|
||||
|
||||
export {
|
||||
Color as default,
|
||||
};
|
||||
|
||||
export {};
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"name": "@img/colour",
|
||||
"version": "1.1.0",
|
||||
"description": "The ESM-only 'color' package made compatible for use with CommonJS runtimes",
|
||||
"license": "MIT",
|
||||
"main": "index.cjs",
|
||||
"types": "index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./index.d.ts",
|
||||
"require": "./index.cjs",
|
||||
"default": "./index.cjs"
|
||||
},
|
||||
"./package.json": "./package.json"
|
||||
},
|
||||
"authors": [
|
||||
"Heather Arthur <fayearthur@gmail.com>",
|
||||
"Josh Junon <josh@junon.me>",
|
||||
"Maxime Thirouin",
|
||||
"Dyma Ywanov <dfcreative@gmail.com>",
|
||||
"LitoMore (https://github.com/LitoMore)"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"files": [
|
||||
"color.cjs",
|
||||
"index.d.ts"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lovell/colour.git"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"keywords": [
|
||||
"color",
|
||||
"colour",
|
||||
"cjs",
|
||||
"commonjs"
|
||||
],
|
||||
"scripts": {
|
||||
"build:cjs": "esbuild node_modules/color/index.js --bundle --platform=node --outfile=color.cjs",
|
||||
"build:dts": "dts-bundle-generator ./dts-src.ts -o index.d.ts --project tsconfig.build.json --external-inlines color --external-inlines color-convert --export-referenced-types=false",
|
||||
"build": "npm run build:cjs && npm run build:dts",
|
||||
"test": "node --test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"color": "5.0.3",
|
||||
"color-convert": "3.1.3",
|
||||
"color-name": "2.1.0",
|
||||
"color-string": "2.1.4",
|
||||
"dts-bundle-generator": "^9.5.1",
|
||||
"esbuild": "^0.27.3"
|
||||
}
|
||||
}
|
||||
+191
@@ -0,0 +1,191 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction, and
|
||||
distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by the copyright
|
||||
owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all other entities
|
||||
that control, are controlled by, or are under common control with that entity.
|
||||
For the purposes of this definition, "control" means (i) the power, direct or
|
||||
indirect, to cause the direction or management of such entity, whether by
|
||||
contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity exercising
|
||||
permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications, including
|
||||
but not limited to software source code, documentation source, and configuration
|
||||
files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical transformation or
|
||||
translation of a Source form, including but not limited to compiled object code,
|
||||
generated documentation, and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or Object form, made
|
||||
available under the License, as indicated by a copyright notice that is included
|
||||
in or attached to the work (an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object form, that
|
||||
is based on (or derived from) the Work and for which the editorial revisions,
|
||||
annotations, elaborations, or other modifications represent, as a whole, an
|
||||
original work of authorship. For the purposes of this License, Derivative Works
|
||||
shall not include works that remain separable from, or merely link (or bind by
|
||||
name) to the interfaces of, the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including the original version
|
||||
of the Work and any modifications or additions to that Work or Derivative Works
|
||||
thereof, that is intentionally submitted to Licensor for inclusion in the Work
|
||||
by the copyright owner or by an individual or Legal Entity authorized to submit
|
||||
on behalf of the copyright owner. For the purposes of this definition,
|
||||
"submitted" means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems, and
|
||||
issue tracking systems that are managed by, or on behalf of, the Licensor for
|
||||
the purpose of discussing and improving the Work, but excluding communication
|
||||
that is conspicuously marked or otherwise designated in writing by the copyright
|
||||
owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity on behalf
|
||||
of whom a Contribution has been received by Licensor and subsequently
|
||||
incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the Work and such
|
||||
Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License.
|
||||
|
||||
Subject to the terms and conditions of this License, each Contributor hereby
|
||||
grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free,
|
||||
irrevocable (except as stated in this section) patent license to make, have
|
||||
made, use, offer to sell, sell, import, and otherwise transfer the Work, where
|
||||
such license applies only to those patent claims licensable by such Contributor
|
||||
that are necessarily infringed by their Contribution(s) alone or by combination
|
||||
of their Contribution(s) with the Work to which such Contribution(s) was
|
||||
submitted. If You institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work or a
|
||||
Contribution incorporated within the Work constitutes direct or contributory
|
||||
patent infringement, then any patent licenses granted to You under this License
|
||||
for that Work shall terminate as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution.
|
||||
|
||||
You may reproduce and distribute copies of the Work or Derivative Works thereof
|
||||
in any medium, with or without modifications, and in Source or Object form,
|
||||
provided that You meet the following conditions:
|
||||
|
||||
You must give any other recipients of the Work or Derivative Works a copy of
|
||||
this License; and
|
||||
You must cause any modified files to carry prominent notices stating that You
|
||||
changed the files; and
|
||||
You must retain, in the Source form of any Derivative Works that You distribute,
|
||||
all copyright, patent, trademark, and attribution notices from the Source form
|
||||
of the Work, excluding those notices that do not pertain to any part of the
|
||||
Derivative Works; and
|
||||
If the Work includes a "NOTICE" text file as part of its distribution, then any
|
||||
Derivative Works that You distribute must include a readable copy of the
|
||||
attribution notices contained within such NOTICE file, excluding those notices
|
||||
that do not pertain to any part of the Derivative Works, in at least one of the
|
||||
following places: within a NOTICE text file distributed as part of the
|
||||
Derivative Works; within the Source form or documentation, if provided along
|
||||
with the Derivative Works; or, within a display generated by the Derivative
|
||||
Works, if and wherever such third-party notices normally appear. The contents of
|
||||
the NOTICE file are for informational purposes only and do not modify the
|
||||
License. You may add Your own attribution notices within Derivative Works that
|
||||
You distribute, alongside or as an addendum to the NOTICE text from the Work,
|
||||
provided that such additional attribution notices cannot be construed as
|
||||
modifying the License.
|
||||
You may add Your own copyright statement to Your modifications and may provide
|
||||
additional or different license terms and conditions for use, reproduction, or
|
||||
distribution of Your modifications, or for any such Derivative Works as a whole,
|
||||
provided Your use, reproduction, and distribution of the Work otherwise complies
|
||||
with the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions.
|
||||
|
||||
Unless You explicitly state otherwise, any Contribution intentionally submitted
|
||||
for inclusion in the Work by You to the Licensor shall be under the terms and
|
||||
conditions of this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify the terms of
|
||||
any separate license agreement you may have executed with Licensor regarding
|
||||
such Contributions.
|
||||
|
||||
6. Trademarks.
|
||||
|
||||
This License does not grant permission to use the trade names, trademarks,
|
||||
service marks, or product names of the Licensor, except as required for
|
||||
reasonable and customary use in describing the origin of the Work and
|
||||
reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty.
|
||||
|
||||
Unless required by applicable law or agreed to in writing, Licensor provides the
|
||||
Work (and each Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied,
|
||||
including, without limitation, any warranties or conditions of TITLE,
|
||||
NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are
|
||||
solely responsible for determining the appropriateness of using or
|
||||
redistributing the Work and assume any risks associated with Your exercise of
|
||||
permissions under this License.
|
||||
|
||||
8. Limitation of Liability.
|
||||
|
||||
In no event and under no legal theory, whether in tort (including negligence),
|
||||
contract, or otherwise, unless required by applicable law (such as deliberate
|
||||
and grossly negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special, incidental,
|
||||
or consequential damages of any character arising as a result of this License or
|
||||
out of the use or inability to use the Work (including but not limited to
|
||||
damages for loss of goodwill, work stoppage, computer failure or malfunction, or
|
||||
any and all other commercial damages or losses), even if such Contributor has
|
||||
been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability.
|
||||
|
||||
While redistributing the Work or Derivative Works thereof, You may choose to
|
||||
offer, and charge a fee for, acceptance of support, warranty, indemnity, or
|
||||
other liability obligations and/or rights consistent with this License. However,
|
||||
in accepting such obligations, You may act only on Your own behalf and on Your
|
||||
sole responsibility, not on behalf of any other Contributor, and only if You
|
||||
agree to indemnify, defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason of your
|
||||
accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work
|
||||
|
||||
To apply the Apache License to your work, attach the following boilerplate
|
||||
notice, with the fields enclosed by brackets "[]" replaced with your own
|
||||
identifying information. (Don't include the brackets!) The text should be
|
||||
enclosed in the appropriate comment syntax for the file format. We also
|
||||
recommend that a file or class name and description of purpose be included on
|
||||
the same "printed page" as the copyright notice for easier identification within
|
||||
third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
# `@img/sharp-win32-x64`
|
||||
|
||||
Prebuilt sharp for use with Windows x64.
|
||||
|
||||
## Licensing
|
||||
|
||||
Copyright 2013 Lovell Fuller and others.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
[https://www.apache.org/licenses/LICENSE-2.0](https://www.apache.org/licenses/LICENSE-2.0)
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
This software contains third-party libraries
|
||||
used under the terms of the following licences:
|
||||
|
||||
| Library | Used under the terms of |
|
||||
|---------------|-----------------------------------------------------------------------------------------------------------|
|
||||
| aom | BSD 2-Clause + [Alliance for Open Media Patent License 1.0](https://aomedia.org/license/patent-license/) |
|
||||
| cairo | Mozilla Public License 2.0 |
|
||||
| cgif | MIT Licence |
|
||||
| expat | MIT Licence |
|
||||
| fontconfig | [fontconfig Licence](https://gitlab.freedesktop.org/fontconfig/fontconfig/blob/main/COPYING) (BSD-like) |
|
||||
| freetype | [freetype Licence](https://git.savannah.gnu.org/cgit/freetype/freetype2.git/tree/docs/FTL.TXT) (BSD-like) |
|
||||
| fribidi | LGPLv3 |
|
||||
| glib | LGPLv3 |
|
||||
| harfbuzz | MIT Licence |
|
||||
| highway | Apache-2.0 License, BSD 3-Clause |
|
||||
| lcms | MIT Licence |
|
||||
| libarchive | BSD 2-Clause |
|
||||
| libexif | LGPLv3 |
|
||||
| libffi | MIT Licence |
|
||||
| libheif | LGPLv3 |
|
||||
| libimagequant | [BSD 2-Clause](https://github.com/lovell/libimagequant/blob/main/COPYRIGHT) |
|
||||
| libnsgif | MIT Licence |
|
||||
| libpng | [libpng License](https://github.com/pnggroup/libpng/blob/master/LICENSE) |
|
||||
| librsvg | LGPLv3 |
|
||||
| libspng | [BSD 2-Clause, libpng License](https://github.com/randy408/libspng/blob/master/LICENSE) |
|
||||
| libtiff | [libtiff License](https://gitlab.com/libtiff/libtiff/blob/master/LICENSE.md) (BSD-like) |
|
||||
| libvips | LGPLv3 |
|
||||
| libwebp | New BSD License |
|
||||
| libxml2 | MIT Licence |
|
||||
| mozjpeg | [zlib License, IJG License, BSD-3-Clause](https://github.com/mozilla/mozjpeg/blob/master/LICENSE.md) |
|
||||
| pango | LGPLv3 |
|
||||
| pixman | MIT Licence |
|
||||
| proxy-libintl | LGPLv3 |
|
||||
| zlib-ng | [zlib Licence](https://github.com/zlib-ng/zlib-ng/blob/develop/LICENSE.md) |
|
||||
|
||||
Use of libraries under the terms of the LGPLv3 is via the
|
||||
"any later version" clause of the LGPLv2 or LGPLv2.1.
|
||||
|
||||
Please report any errors or omissions via
|
||||
https://github.com/lovell/sharp-libvips/issues/new
|
||||
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
+39
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "@img/sharp-win32-x64",
|
||||
"version": "0.34.5",
|
||||
"description": "Prebuilt sharp for use with Windows x64",
|
||||
"author": "Lovell Fuller <npm@lovell.info>",
|
||||
"homepage": "https://sharp.pixelplumbing.com",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/lovell/sharp.git",
|
||||
"directory": "npm/win32-x64"
|
||||
},
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"preferUnplugged": true,
|
||||
"files": [
|
||||
"lib",
|
||||
"versions.json"
|
||||
],
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"type": "commonjs",
|
||||
"exports": {
|
||||
"./sharp.node": "./lib/sharp-win32-x64.node",
|
||||
"./package": "./package.json",
|
||||
"./versions": "./versions.json"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
]
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"aom": "3.13.1",
|
||||
"archive": "3.8.2",
|
||||
"cairo": "1.18.4",
|
||||
"cgif": "0.5.0",
|
||||
"exif": "0.6.25",
|
||||
"expat": "2.7.3",
|
||||
"ffi": "3.5.2",
|
||||
"fontconfig": "2.17.1",
|
||||
"freetype": "2.14.1",
|
||||
"fribidi": "1.0.16",
|
||||
"glib": "2.86.1",
|
||||
"harfbuzz": "12.1.0",
|
||||
"heif": "1.20.2",
|
||||
"highway": "1.3.0",
|
||||
"imagequant": "2.4.1",
|
||||
"lcms": "2.17",
|
||||
"mozjpeg": "0826579",
|
||||
"pango": "1.57.0",
|
||||
"pixman": "0.46.4",
|
||||
"png": "1.6.50",
|
||||
"proxy-libintl": "0.5",
|
||||
"rsvg": "2.61.2",
|
||||
"spng": "0.7.4",
|
||||
"tiff": "4.7.1",
|
||||
"vips": "8.17.3",
|
||||
"webp": "1.6.0",
|
||||
"xml2": "2.15.1",
|
||||
"zlib-ng": "2.2.5"
|
||||
}
|
||||
+3
@@ -0,0 +1,3 @@
|
||||
# `@napi-rs/canvas-win32-x64-msvc`
|
||||
|
||||
This is the **x86_64-pc-windows-msvc** binary for `@napi-rs/canvas`
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+45
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"name": "@napi-rs/canvas-win32-x64-msvc",
|
||||
"version": "0.1.100",
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"main": "skia.win32-x64-msvc.node",
|
||||
"files": [
|
||||
"skia.win32-x64-msvc.node",
|
||||
"icudtl.dat"
|
||||
],
|
||||
"description": "Canvas for Node.js with skia backend",
|
||||
"keywords": [
|
||||
"napi-rs",
|
||||
"NAPI",
|
||||
"N-API",
|
||||
"Rust",
|
||||
"node-addon",
|
||||
"node-addon-api",
|
||||
"canvas",
|
||||
"image",
|
||||
"pdf",
|
||||
"svg",
|
||||
"skia"
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Brooooooklyn/canvas.git"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
+21
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2020 lynweklm@gmail.com
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+448
@@ -0,0 +1,448 @@
|
||||
# `skr canvas`
|
||||
|
||||
[](https://github.com/Brooooooklyn/canvas/actions/workflows/CI.yaml)
|
||||

|
||||
[](https://packagephobia.com/result?p=@napi-rs/canvas)
|
||||
[](https://npmcharts.com/compare/@napi-rs/canvas?minimal=true)
|
||||
|
||||
> 🚀 Help me to become a full-time open-source developer by [sponsoring me on Github](https://github.com/sponsors/Brooooooklyn)
|
||||
|
||||
Google Skia binding to Node.js via [Node-API](https://napi.rs), **0 System dependencies!**
|
||||
|
||||
⚠️ This project is in pre-release stage. And there may be some bugs.<br/>
|
||||
For details on planned features and future direction please refer to the [Roadmap](https://github.com/Brooooooklyn/canvas/issues/113).
|
||||
|
||||
[中文文档](./README-zh.md)
|
||||
|
||||
# Install
|
||||
|
||||
```bash
|
||||
yarn add @napi-rs/canvas
|
||||
npm install @napi-rs/canvas
|
||||
```
|
||||
|
||||
# Support matrix
|
||||
|
||||
## System requirement
|
||||
|
||||
### `arm64`
|
||||
|
||||
[**_cortex-a57_**](https://en.wikipedia.org/wiki/ARM_Cortex-A57) or newer CPU architecture on **Linux**.
|
||||
|
||||
All Apple M chips on **macOS**.
|
||||
|
||||
### `armv7`
|
||||
|
||||
[**_cortex-a7_**](https://en.wikipedia.org/wiki/ARM_Cortex-A7) or newer CPU architecture.
|
||||
|
||||
### glibc
|
||||
|
||||
Since Skia relies on the [glibc](https://www.gnu.org/software/libc/) 2.18 API, you need to have at least glibc version >= 2.18 on your system.
|
||||
|
||||
## AWS Lambda usage
|
||||
|
||||
To use this library on Lambda you will need to use a Lambda layer.
|
||||
|
||||
You can simply attach a lambda layer by getting an ARN from [Canvas-Lambda-Layer](https://github.com/ShivamJoker/Canvas-Lambda-Layer)
|
||||
|
||||
> Make sure to exclude `@napi-rs/canvas` while bundling your Lambda.
|
||||
|
||||
# Usage
|
||||
|
||||
```js
|
||||
const { promises } = require('node:fs')
|
||||
const { join } = require('node:path')
|
||||
const { createCanvas, loadImage } = require('@napi-rs/canvas')
|
||||
|
||||
const canvas = createCanvas(300, 320)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
ctx.lineWidth = 10
|
||||
ctx.strokeStyle = '#03a9f4'
|
||||
ctx.fillStyle = '#03a9f4'
|
||||
|
||||
// Wall
|
||||
ctx.strokeRect(75, 140, 150, 110)
|
||||
|
||||
// Door
|
||||
ctx.fillRect(130, 190, 40, 60)
|
||||
|
||||
// Roof
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(50, 140)
|
||||
ctx.lineTo(150, 60)
|
||||
ctx.lineTo(250, 140)
|
||||
ctx.closePath()
|
||||
ctx.stroke()
|
||||
|
||||
async function main() {
|
||||
// load images from disk or from a URL
|
||||
const catImage = await loadImage('path/to/cat.png')
|
||||
const dogImage = await loadImage('https://example.com/path/to/dog.jpg')
|
||||
|
||||
ctx.drawImage(catImage, 0, 0, catImage.width, catImage.height)
|
||||
|
||||
ctx.drawImage(dogImage, canvas.width / 2, canvas.height / 2, dogImage.width, dogImage.height)
|
||||
|
||||
// export canvas as image
|
||||
const pngData = await canvas.encode('png') // JPEG, AVIF and WebP are also supported
|
||||
// encoding in libuv thread pool, non-blocking
|
||||
await promises.writeFile(join(__dirname, 'simple.png'), pngData)
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
|
||||

|
||||
|
||||
## Emoji text
|
||||
|
||||
```js
|
||||
const { writeFileSync } = require('fs')
|
||||
const { join } = require('path')
|
||||
|
||||
const { createCanvas, GlobalFonts } = require('@napi-rs/canvas')
|
||||
|
||||
GlobalFonts.registerFromPath(join(__dirname, '..', 'fonts', 'AppleColorEmoji@2x.ttf'), 'Apple Emoji')
|
||||
GlobalFonts.registerFromPath(join(__dirname, '..', '__test__', 'fonts', 'COLRv1.ttf'), 'COLRv1')
|
||||
|
||||
console.info(GlobalFonts.families)
|
||||
|
||||
const canvas = createCanvas(760, 360)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
ctx.font = '50px Apple Emoji'
|
||||
ctx.strokeText('😀😃😄😁😆😅😂🤣☺️😊😊😇', 50, 150)
|
||||
|
||||
ctx.font = '100px COLRv1'
|
||||
ctx.fillText('abc', 50, 300)
|
||||
|
||||
const b = canvas.toBuffer('image/png')
|
||||
|
||||
writeFileSync(join(__dirname, 'draw-emoji.png'), b)
|
||||
```
|
||||
|
||||

|
||||
|
||||
# Performance
|
||||
|
||||
See [benchmark](./benchmark) for benchmark code.
|
||||
|
||||
Hardware info:
|
||||
|
||||
```
|
||||
,MMMM. Host - xxxxxxxxxxxxxxxxxxxxxxx
|
||||
.MMMMMM Machine - Mac15,9
|
||||
MMMMM, Kernel - 24.0.0
|
||||
.;MMMMM:' MMMMMMMMMM;. OS - macOS 15.0.1 Sequoia
|
||||
MMMMMMMMMMMMNWMMMMMMMMMMM: DE - Aqua
|
||||
.MMMMMMMMMMMMMMMMMMMMMMMMWM. WM - Quartz Compositor
|
||||
MMMMMMMMMMMMMMMMMMMMMMMMM. Packages - 194 (Homebrew), 32 (cargo)
|
||||
;MMMMMMMMMMMMMMMMMMMMMMMM: Shell - zsh
|
||||
:MMMMMMMMMMMMMMMMMMMMMMMM: Terminal - warpterminal (Version v0.2024.10.23.14.49.stable_00)
|
||||
.MMMMMMMMMMMMMMMMMMMMMMMMM. Resolution - 5120x2880@160fps (as 2560x1440)
|
||||
MMMMMMMMMMMMMMMMMMMMMMMMMMM. 2992x1934@120fps (as 1496x967)
|
||||
.MMMMMMMMMMMMMMMMMMMMMMMMMM. 2232x1512@60fps (as 1116x756)
|
||||
MMMMMMMMMMMMMMMMMMMMMMMM Uptime - 1d 2h 32m
|
||||
;MMMMMMMMMMMMMMMMMMMM. CPU - Apple M3 Max (16)
|
||||
.MMMM,. .MMMM,. CPU Load - 16%
|
||||
Memory - 50.1 GB / 134.2 GB
|
||||
Battery - 78% & Discharging
|
||||
Disk Space - 624.0 GB / 994.7 GB
|
||||
```
|
||||
|
||||
```
|
||||
❯ yarn bench
|
||||
Draw a House and export to PNG
|
||||
┌─────────┬─────────────────┬───────────────────────┬──────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
|
||||
│ (index) │ Task name │ Latency average (ns) │ Latency median (ns) │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
|
||||
├─────────┼─────────────────┼───────────────────────┼──────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
|
||||
│ 0 │ '@napi-rs/skia' │ '14676992.14 ± 0.68%' │ '14602333.00' │ '68 ± 0.59%' │ '68' │ 69 │
|
||||
│ 1 │ 'skia-canvas' │ '21167809.17 ± 2.05%' │ '20960021.00 ± 13646.00' │ '47 ± 1.31%' │ '48' │ 64 │
|
||||
│ 2 │ 'node-canvas' │ '16552027.42 ± 0.70%' │ '16451291.50 ± 2208.50' │ '60 ± 0.62%' │ '61' │ 64 │
|
||||
└─────────┴─────────────────┴───────────────────────┴──────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
|
||||
Draw Gradient and export to PNG
|
||||
┌─────────┬─────────────────┬───────────────────────┬─────────────────────────┬────────────────────────────┬───────────────────────────┬─────────┐
|
||||
│ (index) │ Task name │ Latency average (ns) │ Latency median (ns) │ Throughput average (ops/s) │ Throughput median (ops/s) │ Samples │
|
||||
├─────────┼─────────────────┼───────────────────────┼─────────────────────────┼────────────────────────────┼───────────────────────────┼─────────┤
|
||||
│ 0 │ '@napi-rs/skia' │ '15228495.58 ± 0.53%' │ '15146312.50 ± 1187.50' │ '66 ± 0.48%' │ '66' │ 66 │
|
||||
│ 1 │ 'skia-canvas' │ '21725564.41 ± 2.20%' │ '21412520.50 ± 2104.50' │ '46 ± 1.39%' │ '47' │ 64 │
|
||||
│ 2 │ 'node-canvas' │ '17976022.14 ± 1.53%' │ '17563479.50 ± 5104.50' │ '56 ± 1.38%' │ '57' │ 64 │
|
||||
└─────────┴─────────────────┴───────────────────────┴─────────────────────────┴────────────────────────────┴───────────────────────────┴─────────┘
|
||||
```
|
||||
|
||||
# Features
|
||||
|
||||
## Path2D
|
||||
|
||||
```typescript
|
||||
new Path2D()
|
||||
new Path2D(path: Path2D)
|
||||
// new Path2D('M108.956,403.826c0,0,0.178,3.344-1.276,3.311 c-1.455-0.033-30.507-84.917-66.752-80.957C40.928,326.18,72.326,313.197,108.956,403.826z')
|
||||
new Path2D(path: string)
|
||||
```
|
||||
|
||||
```typescript
|
||||
export interface DOMMatrix2DInit {
|
||||
a: number
|
||||
b: number
|
||||
c: number
|
||||
d: number
|
||||
e: number
|
||||
f: number
|
||||
}
|
||||
|
||||
export class Path2D {
|
||||
constructor(path?: Path2D | string)
|
||||
|
||||
addPath(path: Path2D, transform?: DOMMatrix2DInit): void
|
||||
arc(x: number, y: number, radius: number, startAngle: number, endAngle: number, anticlockwise?: boolean): void
|
||||
arcTo(x1: number, y1: number, x2: number, y2: number, radius: number): void
|
||||
bezierCurveTo(cp1x: number, cp1y: number, cp2x: number, cp2y: number, x: number, y: number): void
|
||||
closePath(): void
|
||||
ellipse(
|
||||
x: number,
|
||||
y: number,
|
||||
radiusX: number,
|
||||
radiusY: number,
|
||||
rotation: number,
|
||||
startAngle: number,
|
||||
endAngle: number,
|
||||
anticlockwise?: boolean,
|
||||
): void
|
||||
lineTo(x: number, y: number): void
|
||||
moveTo(x: number, y: number): void
|
||||
quadraticCurveTo(cpx: number, cpy: number, x: number, y: number): void
|
||||
rect(x: number, y: number, w: number, h: number): void
|
||||
|
||||
// PathKit methods
|
||||
op(path: Path2D, operation: PathOp): Path2D
|
||||
toSVGString(): string
|
||||
getFillType(): FillType
|
||||
getFillTypeString(): string
|
||||
setFillType(type: FillType): void
|
||||
simplify(): Path2D
|
||||
asWinding(): Path2D
|
||||
stroke(stroke?: StrokeOptions): Path2D
|
||||
transform(transform: DOMMatrix2DInit): Path2D
|
||||
getBounds(): [left: number, top: number, right: number, bottom: number]
|
||||
computeTightBounds(): [left: number, top: number, right: number, bottom: number]
|
||||
trim(start: number, end: number, isComplement?: boolean): Path2D
|
||||
round(radius: number): Path2D
|
||||
equals(path: Path2D): boolean
|
||||
}
|
||||
```
|
||||
|
||||
## PathKit
|
||||
|
||||
`PathKit` is a toolset for manipulating Path in `Skia`, supporting **_quadratic beziers_**, **_cubic beziers_** and **_conics_**.
|
||||
The main features are.
|
||||
|
||||
### Path Operation
|
||||
|
||||
`.op(path, PathOp)`
|
||||
|
||||
```js
|
||||
const pathOne = new Path2D(
|
||||
'M8 50H92C96.4183 50 100 53.5817 100 58V142C100 146.418 96.4183 150 92 150H8C3.58172 150 0 146.418 0 142V58C0 53.5817 3.58172 50 8 50Z',
|
||||
)
|
||||
const pathTwo = new Path2D(
|
||||
'"M58 0H142C146.418 0 150 3.58172 150 8V92C150 96.4183 146.418 100 142 100H58C53.5817 100 50 96.4183 50 92V8C50 3.58172 53.5817 0 58 0Z',
|
||||
)
|
||||
|
||||
pathOne.op(pathTwo, PathOp.Intersect).toSVGString()
|
||||
// => "M100 100L58 100C53.5817 100 50 96.4183 50 92L50 50L92 50C96.4183 50 100 53.5817 100 58L100 100Z"
|
||||
```
|
||||
|
||||
- **Union**, subtract the op path from the first path
|
||||
- **Difference**, intersect the two paths
|
||||
- **ReverseDifference**, union (inclusive-or) the two paths
|
||||
- **Intersect**, exclusive-or the two paths
|
||||
- **XOR**, subtract the first path from the op path
|
||||
|
||||

|
||||
|
||||
### Covert `FillType` in **_Path_**
|
||||
|
||||
`.asWinding()`
|
||||
|
||||
You can convert `fill-rule="evenodd"` to `fill-rule="nonzero"` in SVG.
|
||||
This is useful for **OpenType** font-related tools, as `fill-rule="nonzero"` is only supported in **OpenType** fonts.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
const pathCircle = new Path2D(
|
||||
'M24.2979 13.6364H129.394V40.9091H24.2979L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455H70.4673V68.1818H16.073C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727V113.636C7.00728 118.657 11.0661 122.727 16.073 122.727H70.4673V150H84.0658V122.727H128.041C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818H84.0658V54.5455H133.927C138.934 54.5455 142.993 50.4755 142.993 45.4545V9.09091C142.993 4.07014 138.934 0 133.927 0H21.9592ZM125.702 109.091H20.6058V81.8182H125.702L135.372 95.4545L125.702 109.091Z',
|
||||
)
|
||||
pathCircle.setFillType(FillType.EvenOdd)
|
||||
pathCircle.asWinding().toSVGString()
|
||||
// => "M24.2979 13.6364L129.394 13.6364L129.394 40.9091L24.2979 40.9091L14.6278 27.2727L24.2979 13.6364ZM21.9592 0C19.0246 0 16.2716 1.42436 14.571 3.82251L1.67756 22.0043C-0.559186 25.1585 -0.559186 29.387 1.67756 32.5411L14.571 50.7227C16.2716 53.1209 19.0246 54.5455 21.9592 54.5455L70.4673 54.5455L70.4673 68.1818L16.073 68.1818C11.0661 68.1818 7.00728 72.2518 7.00728 77.2727L7.00728 113.636C7.00728 118.657 11.0661 122.727 16.073 122.727L70.4673 122.727L70.4673 150L84.0658 150L84.0658 122.727L128.041 122.727C130.975 122.727 133.729 121.303 135.429 118.905L148.323 100.723C150.559 97.5686 150.559 93.3405 148.323 90.1864L135.429 72.0045C133.729 69.6064 130.975 68.1818 128.041 68.1818L84.0658 68.1818L84.0658 54.5455L133.927 54.5455C138.934 54.5455 142.993 50.4755 142.993 45.4545L142.993 9.09091C142.993 4.07014 138.934 0 133.927 0L21.9592 0ZM125.702 109.091L20.6058 109.091L20.6058 81.8182L125.702 81.8182L135.372 95.4545L125.702 109.091Z"
|
||||
```
|
||||
|
||||
### Simplify **_Path_**
|
||||
|
||||
`.simplify()`
|
||||
|
||||
Set the path to the same non-overlapping contour as the original path area, which means that it can also remove overlapping paths.
|
||||
|
||||
<img width="800" src="./docs/imgs/simplify.png" >
|
||||
|
||||
[SVG with overlapping paths](./docs/imgs/overlapping-path.svg) (Left)
|
||||
|
||||
```js
|
||||
const path =
|
||||
'M2.933,89.89 L89.005,3.818 Q90.412,2.411 92.249,1.65 Q94.087,0.889 96.076,0.889 Q98.065,0.889 99.903,1.65 Q101.741,2.411 103.147,3.818 L189.22,89.89 Q190.626,91.296 191.387,93.134 Q192.148,94.972 192.148,96.961 Q192.148,98.95 191.387,100.788 Q190.626,102.625 189.219,104.032 Q187.813,105.439 185.975,106.2 Q184.138,106.961 182.148,106.961 Q180.159,106.961 178.322,106.2 Q176.484,105.439 175.077,104.032 L89.005,17.96 L96.076,10.889 L103.147,17.96 L17.075,104.032 Q15.668,105.439 13.831,106.2 Q11.993,106.961 10.004,106.961 Q8.015,106.961 6.177,106.2 Q4.339,105.439 2.933,104.032 Q1.526,102.625 0.765,100.788 Q0.004,98.95 0.004,96.961 Q0.004,94.972 0.765,93.134 Q1.526,91.296 2.933,89.89 Z'
|
||||
|
||||
path.simplify().toSVGString()
|
||||
// => "M89.005 3.818L2.933 89.89Q1.526 91.296 0.765 93.134Q0.004 94.972 0.004 96.961Q0.004 98.95 0.765 100.788Q1.526 102.625 2.933 104.032Q4.339 105.439 6.177 106.2Q8.015 106.961 10.004 106.961Q11.993 106.961 13.831 106.2Q15.668 105.439 17.075 104.032L96.076 25.031L175.077 104.032Q176.484 105.439 178.322 106.2Q180.159 106.961 182.148 106.961Q184.138 106.961 185.975 106.2Q187.813 105.439 189.219 104.032Q190.626 102.625 191.387 100.788Q192.148 98.95 192.148 96.961Q192.148 94.972 191.387 93.134Q190.626 91.296 189.22 89.89L103.147 3.818Q101.741 2.411 99.903 1.65Q98.065 0.889 96.076 0.889Q94.087 0.889 92.249 1.65Q90.412 2.411 89.005 3.818Z"
|
||||
```
|
||||
|
||||
## Lottie Animation
|
||||
|
||||
Render [Lottie](https://airbnb.io/lottie/) animations using Skia's [Skottie](https://skia.org/docs/user/modules/skottie/) module.
|
||||
|
||||
### Load Animation
|
||||
|
||||
```js
|
||||
const { LottieAnimation } = require('@napi-rs/canvas')
|
||||
|
||||
// Load from file
|
||||
const animation = LottieAnimation.loadFromFile('animation.json')
|
||||
|
||||
// Load from JSON string with resource path for external assets
|
||||
const animation = LottieAnimation.loadFromData(jsonString, {
|
||||
resourcePath: '/path/to/assets',
|
||||
})
|
||||
```
|
||||
|
||||
### Animation Properties
|
||||
|
||||
```js
|
||||
animation.duration // Total duration in seconds
|
||||
animation.fps // Frames per second
|
||||
animation.frames // Total frame count
|
||||
animation.width // Animation width
|
||||
animation.height // Animation height
|
||||
animation.version // Lottie format version
|
||||
```
|
||||
|
||||
### Playback Control
|
||||
|
||||
```js
|
||||
animation.seekFrame(30) // Seek to frame 30
|
||||
animation.seek(1.5) // Seek to 1.5 seconds
|
||||
```
|
||||
|
||||
### Render to Canvas
|
||||
|
||||
```js
|
||||
const { createCanvas, LottieAnimation } = require('@napi-rs/canvas')
|
||||
|
||||
const animation = LottieAnimation.loadFromFile('animation.json')
|
||||
const canvas = createCanvas(animation.width, animation.height)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
// Render at original size
|
||||
animation.render(ctx)
|
||||
|
||||
// Render with custom destination rect
|
||||
animation.render(ctx, { x: 0, y: 0, width: 800, height: 600 })
|
||||
```
|
||||
|
||||
### Supported Features
|
||||
|
||||
- **Embedded images** - Base64-encoded images (`data:image/png;base64,...`)
|
||||
- **Embedded fonts** - Vector glyph paths for text rendering without system fonts
|
||||
- **External assets** - Load images from `resourcePath` directory
|
||||
- **dotLottie format** - Extract `.lottie` ZIP files at runtime (see example)
|
||||
|
||||
### Example: Encode Lottie to Video
|
||||
|
||||
See [`example/lottie-to-video.ts`](./example/lottie-to-video.ts) for encoding Lottie animations to MP4 using [`@napi-rs/webcodecs`](https://github.com/Brooooooklyn/webcodecs-node).
|
||||
|
||||
```js
|
||||
import { createCanvas, LottieAnimation } from '@napi-rs/canvas'
|
||||
import {
|
||||
VideoEncoder,
|
||||
VideoFrame,
|
||||
Mp4Muxer,
|
||||
type EncodedVideoChunk,
|
||||
type EncodedVideoChunkMetadata,
|
||||
} from '@napi-rs/webcodecs'
|
||||
|
||||
const animation = LottieAnimation.loadFromFile('animation.json')
|
||||
const canvas = createCanvas(animation.width, animation.height)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
for (let frame = 0; frame < animation.frames; frame++) {
|
||||
animation.seekFrame(frame)
|
||||
ctx.fillStyle = '#ffffff'
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height)
|
||||
animation.render(ctx)
|
||||
// Encode frame to video...
|
||||
}
|
||||
```
|
||||
|
||||
# [Example](./example/tiger.js)
|
||||
|
||||
> The tiger.json was serialized from [gojs/samples/tiger](https://github.com/NorthwoodsSoftware/GoJS/blob/master/samples/tiger.html)
|
||||
|
||||
<img width="500" src="example/tiger.png">
|
||||
|
||||
```shell
|
||||
node example/anime-girl.js
|
||||
```
|
||||
|
||||
| SVG | PNG |
|
||||
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| <img width="500" src="example/anime-girl.svg"><br/>[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) | <img width="500" src="example/anime-girl.png"><br/>[CC-BY-SA 3.0](https://creativecommons.org/licenses/by/3.0) by [Niabot](https://commons.wikimedia.org/wiki/User:Niabot) |
|
||||
|
||||
# Building
|
||||
|
||||
## Build skia from source
|
||||
|
||||
You can build this project from source, the system requirements are here: https://skia.org/docs/user/build
|
||||
|
||||
```sh
|
||||
# Clone the code:
|
||||
$ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
|
||||
$ cd canvas
|
||||
|
||||
# Build Skia:
|
||||
$ node scripts/build-skia.js
|
||||
|
||||
# Install NPM packages, build the Node.js addon:
|
||||
$ npm install -g yarn
|
||||
$ yarn install --mode=skip-build # Here are modules that are used for benchmarking and are hard to install, you can skip it by specifying `--mode=skip-build`
|
||||
$ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
|
||||
$ yarn build
|
||||
|
||||
# All done! Run test cases or examples now:
|
||||
$ yarn test
|
||||
$ node example/tiger.js
|
||||
```
|
||||
|
||||
## Pull pre-build skia binary from GitHub
|
||||
|
||||
You can pull skia pre-build binaries if you just care the `Rust` part:
|
||||
|
||||
```sh
|
||||
# Clone the code:
|
||||
$ git clone --recurse-submodules https://github.com/Brooooooklyn/canvas.git
|
||||
$ cd canvas
|
||||
|
||||
# Download Skia binaries:
|
||||
# It will pull the binaries match the git hash in `./skia` submodule
|
||||
$ node scripts/release-skia-binary.mjs --download
|
||||
|
||||
# Install NPM packages, build the Node.js addon:
|
||||
$ npm install -g yarn
|
||||
$ yarn install --mode=skip-build
|
||||
$ sudo dnf install clang # https://fedora.pkgs.org/34/fedora-x86_64/clang-12.0.0-0.3.rc1.fc34.x86_64.rpm.html
|
||||
$ yarn build
|
||||
|
||||
# All done! Run test cases or examples now:
|
||||
$ yarn test
|
||||
$ node example/tiger.js
|
||||
```
|
||||
+873
@@ -0,0 +1,873 @@
|
||||
const { inspect } = require('util')
|
||||
/*
|
||||
* vendored in order to fix its dependence on the window global [cds 2020/08/04]
|
||||
* otherwise unchanged from https://github.com/jarek-foksa/geometry-polyfill/tree/f36bbc8f4bc43539d980687904ce46c8e915543d
|
||||
*/
|
||||
|
||||
// @info
|
||||
// DOMPoint polyfill
|
||||
// @src
|
||||
// https://drafts.fxtf.org/geometry/#DOMPoint
|
||||
// https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_point_read_only.cc
|
||||
class DOMPoint {
|
||||
constructor(x = 0, y = 0, z = 0, w = 1) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.z = z
|
||||
this.w = w
|
||||
}
|
||||
|
||||
static fromPoint(otherPoint) {
|
||||
return new DOMPoint(
|
||||
otherPoint.x,
|
||||
otherPoint.y,
|
||||
otherPoint.z !== undefined ? otherPoint.z : 0,
|
||||
otherPoint.w !== undefined ? otherPoint.w : 1,
|
||||
)
|
||||
}
|
||||
|
||||
matrixTransform(matrix) {
|
||||
if ((matrix.is2D || matrix instanceof SVGMatrix) && this.z === 0 && this.w === 1) {
|
||||
return new DOMPoint(
|
||||
this.x * matrix.a + this.y * matrix.c + matrix.e,
|
||||
this.x * matrix.b + this.y * matrix.d + matrix.f,
|
||||
0,
|
||||
1,
|
||||
)
|
||||
} else {
|
||||
return new DOMPoint(
|
||||
this.x * matrix.m11 + this.y * matrix.m21 + this.z * matrix.m31 + this.w * matrix.m41,
|
||||
this.x * matrix.m12 + this.y * matrix.m22 + this.z * matrix.m32 + this.w * matrix.m42,
|
||||
this.x * matrix.m13 + this.y * matrix.m23 + this.z * matrix.m33 + this.w * matrix.m43,
|
||||
this.x * matrix.m14 + this.y * matrix.m24 + this.z * matrix.m34 + this.w * matrix.m44,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
z: this.z,
|
||||
w: this.w,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// @info
|
||||
// DOMRect polyfill
|
||||
// @src
|
||||
// https://drafts.fxtf.org/geometry/#DOMRect
|
||||
// https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_rect_read_only.cc
|
||||
|
||||
class DOMRect {
|
||||
constructor(x = 0, y = 0, width = 0, height = 0) {
|
||||
this.x = x
|
||||
this.y = y
|
||||
this.width = width
|
||||
this.height = height
|
||||
}
|
||||
|
||||
static fromRect(otherRect) {
|
||||
return new DOMRect(otherRect.x, otherRect.y, otherRect.width, otherRect.height)
|
||||
}
|
||||
|
||||
get top() {
|
||||
return this.y
|
||||
}
|
||||
|
||||
get left() {
|
||||
return this.x
|
||||
}
|
||||
|
||||
get right() {
|
||||
return this.x + this.width
|
||||
}
|
||||
|
||||
get bottom() {
|
||||
return this.y + this.height
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
x: this.x,
|
||||
y: this.y,
|
||||
width: this.width,
|
||||
height: this.height,
|
||||
top: this.top,
|
||||
left: this.left,
|
||||
right: this.right,
|
||||
bottom: this.bottom,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const propertyName of ['top', 'right', 'bottom', 'left']) {
|
||||
const propertyDescriptor = Object.getOwnPropertyDescriptor(DOMRect.prototype, propertyName)
|
||||
propertyDescriptor.enumerable = true
|
||||
Object.defineProperty(DOMRect.prototype, propertyName, propertyDescriptor)
|
||||
}
|
||||
|
||||
// @info
|
||||
// DOMMatrix polyfill (SVG 2)
|
||||
// @src
|
||||
// https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_matrix_read_only.cc
|
||||
// https://github.com/tocharomera/generativecanvas/blob/master/node-canvas/lib/DOMMatrix.js
|
||||
|
||||
const M11 = 0,
|
||||
M12 = 1,
|
||||
M13 = 2,
|
||||
M14 = 3
|
||||
const M21 = 4,
|
||||
M22 = 5,
|
||||
M23 = 6,
|
||||
M24 = 7
|
||||
const M31 = 8,
|
||||
M32 = 9,
|
||||
M33 = 10,
|
||||
M34 = 11
|
||||
const M41 = 12,
|
||||
M42 = 13,
|
||||
M43 = 14,
|
||||
M44 = 15
|
||||
|
||||
const A = M11,
|
||||
B = M12
|
||||
const C = M21,
|
||||
D = M22
|
||||
const E = M41,
|
||||
F = M42
|
||||
|
||||
const DEGREE_PER_RAD = 180 / Math.PI
|
||||
const RAD_PER_DEGREE = Math.PI / 180
|
||||
|
||||
const VALUES = Symbol('values')
|
||||
const IS_2D = Symbol('is2D')
|
||||
|
||||
function parseMatrix(init) {
|
||||
let parsed = init.replace(/matrix\(/, '').split(/,/, 7)
|
||||
|
||||
if (parsed.length !== 6) {
|
||||
throw new Error(`Failed to parse ${init}`)
|
||||
}
|
||||
|
||||
parsed = parsed.map(parseFloat)
|
||||
|
||||
return [parsed[0], parsed[1], 0, 0, parsed[2], parsed[3], 0, 0, 0, 0, 1, 0, parsed[4], parsed[5], 0, 1]
|
||||
}
|
||||
|
||||
function parseMatrix3d(init) {
|
||||
const parsed = init.replace(/matrix3d\(/, '').split(/,/, 17)
|
||||
|
||||
if (parsed.length !== 16) {
|
||||
throw new Error(`Failed to parse ${init}`)
|
||||
}
|
||||
|
||||
return parsed.map(parseFloat)
|
||||
}
|
||||
|
||||
function parseTransform(tform) {
|
||||
const type = tform.split(/\(/, 1)[0]
|
||||
|
||||
if (type === 'matrix') {
|
||||
return parseMatrix(tform)
|
||||
} else if (type === 'matrix3d') {
|
||||
return parseMatrix3d(tform)
|
||||
} else {
|
||||
throw new Error(`${type} parsing not implemented`)
|
||||
}
|
||||
}
|
||||
|
||||
const setNumber2D = (receiver, index, value) => {
|
||||
if (typeof value !== 'number') {
|
||||
throw new TypeError('Expected number')
|
||||
}
|
||||
|
||||
receiver[VALUES][index] = value
|
||||
}
|
||||
|
||||
const setNumber3D = (receiver, index, value) => {
|
||||
if (typeof value !== 'number') {
|
||||
throw new TypeError('Expected number')
|
||||
}
|
||||
|
||||
if (index === M33 || index === M44) {
|
||||
if (value !== 1) {
|
||||
receiver[IS_2D] = false
|
||||
}
|
||||
} else if (value !== 0) {
|
||||
receiver[IS_2D] = false
|
||||
}
|
||||
|
||||
receiver[VALUES][index] = value
|
||||
}
|
||||
|
||||
const newInstance = (values) => {
|
||||
const instance = Object.create(DOMMatrix.prototype)
|
||||
instance.constructor = DOMMatrix
|
||||
instance[IS_2D] = true
|
||||
instance[VALUES] = values
|
||||
|
||||
return instance
|
||||
}
|
||||
|
||||
const multiply = (first, second) => {
|
||||
const dest = new Float64Array(16)
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
for (let j = 0; j < 4; j++) {
|
||||
let sum = 0
|
||||
|
||||
for (let k = 0; k < 4; k++) {
|
||||
sum += first[i * 4 + k] * second[k * 4 + j]
|
||||
}
|
||||
|
||||
dest[i * 4 + j] = sum
|
||||
}
|
||||
}
|
||||
|
||||
return dest
|
||||
}
|
||||
|
||||
class DOMMatrix {
|
||||
get m11() {
|
||||
return this[VALUES][M11]
|
||||
}
|
||||
set m11(value) {
|
||||
setNumber2D(this, M11, value)
|
||||
}
|
||||
get m12() {
|
||||
return this[VALUES][M12]
|
||||
}
|
||||
set m12(value) {
|
||||
setNumber2D(this, M12, value)
|
||||
}
|
||||
get m13() {
|
||||
return this[VALUES][M13]
|
||||
}
|
||||
set m13(value) {
|
||||
setNumber3D(this, M13, value)
|
||||
}
|
||||
get m14() {
|
||||
return this[VALUES][M14]
|
||||
}
|
||||
set m14(value) {
|
||||
setNumber3D(this, M14, value)
|
||||
}
|
||||
get m21() {
|
||||
return this[VALUES][M21]
|
||||
}
|
||||
set m21(value) {
|
||||
setNumber2D(this, M21, value)
|
||||
}
|
||||
get m22() {
|
||||
return this[VALUES][M22]
|
||||
}
|
||||
set m22(value) {
|
||||
setNumber2D(this, M22, value)
|
||||
}
|
||||
get m23() {
|
||||
return this[VALUES][M23]
|
||||
}
|
||||
set m23(value) {
|
||||
setNumber3D(this, M23, value)
|
||||
}
|
||||
get m24() {
|
||||
return this[VALUES][M24]
|
||||
}
|
||||
set m24(value) {
|
||||
setNumber3D(this, M24, value)
|
||||
}
|
||||
get m31() {
|
||||
return this[VALUES][M31]
|
||||
}
|
||||
set m31(value) {
|
||||
setNumber3D(this, M31, value)
|
||||
}
|
||||
get m32() {
|
||||
return this[VALUES][M32]
|
||||
}
|
||||
set m32(value) {
|
||||
setNumber3D(this, M32, value)
|
||||
}
|
||||
get m33() {
|
||||
return this[VALUES][M33]
|
||||
}
|
||||
set m33(value) {
|
||||
setNumber3D(this, M33, value)
|
||||
}
|
||||
get m34() {
|
||||
return this[VALUES][M34]
|
||||
}
|
||||
set m34(value) {
|
||||
setNumber3D(this, M34, value)
|
||||
}
|
||||
get m41() {
|
||||
return this[VALUES][M41]
|
||||
}
|
||||
set m41(value) {
|
||||
setNumber2D(this, M41, value)
|
||||
}
|
||||
get m42() {
|
||||
return this[VALUES][M42]
|
||||
}
|
||||
set m42(value) {
|
||||
setNumber2D(this, M42, value)
|
||||
}
|
||||
get m43() {
|
||||
return this[VALUES][M43]
|
||||
}
|
||||
set m43(value) {
|
||||
setNumber3D(this, M43, value)
|
||||
}
|
||||
get m44() {
|
||||
return this[VALUES][M44]
|
||||
}
|
||||
set m44(value) {
|
||||
setNumber3D(this, M44, value)
|
||||
}
|
||||
|
||||
get a() {
|
||||
return this[VALUES][A]
|
||||
}
|
||||
set a(value) {
|
||||
setNumber2D(this, A, value)
|
||||
}
|
||||
get b() {
|
||||
return this[VALUES][B]
|
||||
}
|
||||
set b(value) {
|
||||
setNumber2D(this, B, value)
|
||||
}
|
||||
get c() {
|
||||
return this[VALUES][C]
|
||||
}
|
||||
set c(value) {
|
||||
setNumber2D(this, C, value)
|
||||
}
|
||||
get d() {
|
||||
return this[VALUES][D]
|
||||
}
|
||||
set d(value) {
|
||||
setNumber2D(this, D, value)
|
||||
}
|
||||
get e() {
|
||||
return this[VALUES][E]
|
||||
}
|
||||
set e(value) {
|
||||
setNumber2D(this, E, value)
|
||||
}
|
||||
get f() {
|
||||
return this[VALUES][F]
|
||||
}
|
||||
set f(value) {
|
||||
setNumber2D(this, F, value)
|
||||
}
|
||||
|
||||
get is2D() {
|
||||
return this[IS_2D]
|
||||
}
|
||||
|
||||
get isIdentity() {
|
||||
const values = this[VALUES]
|
||||
|
||||
return (
|
||||
values[M11] === 1 &&
|
||||
values[M12] === 0 &&
|
||||
values[M13] === 0 &&
|
||||
values[M14] === 0 &&
|
||||
values[M21] === 0 &&
|
||||
values[M22] === 1 &&
|
||||
values[M23] === 0 &&
|
||||
values[M24] === 0 &&
|
||||
values[M31] === 0 &&
|
||||
values[M32] === 0 &&
|
||||
values[M33] === 1 &&
|
||||
values[M34] === 0 &&
|
||||
values[M41] === 0 &&
|
||||
values[M42] === 0 &&
|
||||
values[M43] === 0 &&
|
||||
values[M44] === 1
|
||||
)
|
||||
}
|
||||
|
||||
static fromMatrix(init) {
|
||||
if (init instanceof DOMMatrix) {
|
||||
return new DOMMatrix(init[VALUES])
|
||||
} else if (init instanceof SVGMatrix) {
|
||||
return new DOMMatrix([init.a, init.b, init.c, init.d, init.e, init.f])
|
||||
} else {
|
||||
throw new TypeError('Expected DOMMatrix')
|
||||
}
|
||||
}
|
||||
|
||||
static fromFloat32Array(init) {
|
||||
if (!(init instanceof Float32Array)) throw new TypeError('Expected Float32Array')
|
||||
return new DOMMatrix(init)
|
||||
}
|
||||
|
||||
static fromFloat64Array(init) {
|
||||
if (!(init instanceof Float64Array)) throw new TypeError('Expected Float64Array')
|
||||
return new DOMMatrix(init)
|
||||
}
|
||||
|
||||
// @type
|
||||
// (Float64Array) => void
|
||||
constructor(init) {
|
||||
this[IS_2D] = true
|
||||
|
||||
this[VALUES] = new Float64Array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1])
|
||||
|
||||
// Parse CSS transformList
|
||||
if (typeof init === 'string') {
|
||||
if (init === '') {
|
||||
return
|
||||
} else {
|
||||
const tforms = init.split(/\)\s+/, 20).map(parseTransform)
|
||||
|
||||
if (tforms.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
init = tforms[0]
|
||||
|
||||
for (let i = 1; i < tforms.length; i++) {
|
||||
init = multiply(tforms[i], init)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let i = 0
|
||||
|
||||
if (init && init.length === 6) {
|
||||
setNumber2D(this, A, init[i++])
|
||||
setNumber2D(this, B, init[i++])
|
||||
setNumber2D(this, C, init[i++])
|
||||
setNumber2D(this, D, init[i++])
|
||||
setNumber2D(this, E, init[i++])
|
||||
setNumber2D(this, F, init[i++])
|
||||
} else if (init && init.length === 16) {
|
||||
setNumber2D(this, M11, init[i++])
|
||||
setNumber2D(this, M12, init[i++])
|
||||
setNumber3D(this, M13, init[i++])
|
||||
setNumber3D(this, M14, init[i++])
|
||||
setNumber2D(this, M21, init[i++])
|
||||
setNumber2D(this, M22, init[i++])
|
||||
setNumber3D(this, M23, init[i++])
|
||||
setNumber3D(this, M24, init[i++])
|
||||
setNumber3D(this, M31, init[i++])
|
||||
setNumber3D(this, M32, init[i++])
|
||||
setNumber3D(this, M33, init[i++])
|
||||
setNumber3D(this, M34, init[i++])
|
||||
setNumber2D(this, M41, init[i++])
|
||||
setNumber2D(this, M42, init[i++])
|
||||
setNumber3D(this, M43, init[i++])
|
||||
setNumber3D(this, M44, init[i])
|
||||
} else if (init !== undefined) {
|
||||
throw new TypeError('Expected string or array.')
|
||||
}
|
||||
}
|
||||
|
||||
dump() {
|
||||
const mat = this[VALUES]
|
||||
console.info([mat.slice(0, 4), mat.slice(4, 8), mat.slice(8, 12), mat.slice(12, 16)])
|
||||
}
|
||||
|
||||
[inspect.custom](depth) {
|
||||
if (depth < 0) return '[DOMMatrix]'
|
||||
|
||||
const { a, b, c, d, e, f, is2D, isIdentity } = this
|
||||
if (this.is2D) {
|
||||
return `DOMMatrix ${inspect({ a, b, c, d, e, f, is2D, isIdentity }, { colors: true })}`
|
||||
} else {
|
||||
const { m11, m12, m13, m14, m21, m22, m23, m24, m31, m32, m33, m34, m41, m42, m43, m44, is2D, isIdentity } = this
|
||||
return `DOMMatrix ${inspect(
|
||||
{
|
||||
a,
|
||||
b,
|
||||
c,
|
||||
d,
|
||||
e,
|
||||
f,
|
||||
m11,
|
||||
m12,
|
||||
m13,
|
||||
m14,
|
||||
m21,
|
||||
m22,
|
||||
m23,
|
||||
m24,
|
||||
m31,
|
||||
m32,
|
||||
m33,
|
||||
m34,
|
||||
m41,
|
||||
m42,
|
||||
m43,
|
||||
m44,
|
||||
is2D,
|
||||
isIdentity,
|
||||
},
|
||||
{ colors: true },
|
||||
)}`
|
||||
}
|
||||
}
|
||||
|
||||
multiply(other) {
|
||||
return newInstance(this[VALUES]).multiplySelf(other)
|
||||
}
|
||||
|
||||
multiplySelf(other) {
|
||||
this[VALUES] = multiply(other[VALUES], this[VALUES])
|
||||
|
||||
if (!other.is2D) {
|
||||
this[IS_2D] = false
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
preMultiplySelf(other) {
|
||||
this[VALUES] = multiply(this[VALUES], other[VALUES])
|
||||
|
||||
if (!other.is2D) {
|
||||
this[IS_2D] = false
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
translate(tx, ty, tz) {
|
||||
return newInstance(this[VALUES]).translateSelf(tx, ty, tz)
|
||||
}
|
||||
|
||||
translateSelf(tx = 0, ty = 0, tz = 0) {
|
||||
this[VALUES] = multiply([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1], this[VALUES])
|
||||
|
||||
if (tz !== 0) {
|
||||
this[IS_2D] = false
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
scale(scaleX, scaleY, scaleZ, originX, originY, originZ) {
|
||||
return newInstance(this[VALUES]).scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ)
|
||||
}
|
||||
|
||||
scale3d(scale, originX, originY, originZ) {
|
||||
return newInstance(this[VALUES]).scale3dSelf(scale, originX, originY, originZ)
|
||||
}
|
||||
|
||||
scale3dSelf(scale, originX, originY, originZ) {
|
||||
return this.scaleSelf(scale, scale, scale, originX, originY, originZ)
|
||||
}
|
||||
|
||||
scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) {
|
||||
// Not redundant with translate's checks because we need to negate the values later.
|
||||
if (typeof originX !== 'number') originX = 0
|
||||
if (typeof originY !== 'number') originY = 0
|
||||
if (typeof originZ !== 'number') originZ = 0
|
||||
|
||||
this.translateSelf(originX, originY, originZ)
|
||||
|
||||
if (typeof scaleX !== 'number') scaleX = 1
|
||||
if (typeof scaleY !== 'number') scaleY = scaleX
|
||||
if (typeof scaleZ !== 'number') scaleZ = 1
|
||||
|
||||
this[VALUES] = multiply([scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1], this[VALUES])
|
||||
|
||||
this.translateSelf(-originX, -originY, -originZ)
|
||||
|
||||
if (scaleZ !== 1 || originZ !== 0) {
|
||||
this[IS_2D] = false
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
rotateFromVector(x, y) {
|
||||
return newInstance(this[VALUES]).rotateFromVectorSelf(x, y)
|
||||
}
|
||||
|
||||
rotateFromVectorSelf(x = 0, y = 0) {
|
||||
const theta = x === 0 && y === 0 ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD
|
||||
return this.rotateSelf(theta)
|
||||
}
|
||||
|
||||
rotate(rotX, rotY, rotZ) {
|
||||
return newInstance(this[VALUES]).rotateSelf(rotX, rotY, rotZ)
|
||||
}
|
||||
|
||||
rotateSelf(rotX, rotY, rotZ) {
|
||||
if (rotY === undefined && rotZ === undefined) {
|
||||
rotZ = rotX
|
||||
rotX = rotY = 0
|
||||
}
|
||||
|
||||
if (typeof rotY !== 'number') rotY = 0
|
||||
if (typeof rotZ !== 'number') rotZ = 0
|
||||
|
||||
if (rotX !== 0 || rotY !== 0) {
|
||||
this[IS_2D] = false
|
||||
}
|
||||
|
||||
rotX *= RAD_PER_DEGREE
|
||||
rotY *= RAD_PER_DEGREE
|
||||
rotZ *= RAD_PER_DEGREE
|
||||
|
||||
let c = Math.cos(rotZ)
|
||||
let s = Math.sin(rotZ)
|
||||
|
||||
this[VALUES] = multiply([c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES])
|
||||
|
||||
c = Math.cos(rotY)
|
||||
s = Math.sin(rotY)
|
||||
|
||||
this[VALUES] = multiply([c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1], this[VALUES])
|
||||
|
||||
c = Math.cos(rotX)
|
||||
s = Math.sin(rotX)
|
||||
|
||||
this[VALUES] = multiply([1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1], this[VALUES])
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
rotateAxisAngle(x, y, z, angle) {
|
||||
return newInstance(this[VALUES]).rotateAxisAngleSelf(x, y, z, angle)
|
||||
}
|
||||
|
||||
rotateAxisAngleSelf(x = 0, y = 0, z = 0, angle = 0) {
|
||||
const length = Math.sqrt(x * x + y * y + z * z)
|
||||
|
||||
if (length === 0) {
|
||||
return this
|
||||
}
|
||||
|
||||
if (length !== 1) {
|
||||
x /= length
|
||||
y /= length
|
||||
z /= length
|
||||
}
|
||||
|
||||
angle *= RAD_PER_DEGREE
|
||||
|
||||
const c = Math.cos(angle)
|
||||
const s = Math.sin(angle)
|
||||
const t = 1 - c
|
||||
const tx = t * x
|
||||
const ty = t * y
|
||||
|
||||
this[VALUES] = multiply(
|
||||
[
|
||||
tx * x + c,
|
||||
tx * y + s * z,
|
||||
tx * z - s * y,
|
||||
0,
|
||||
tx * y - s * z,
|
||||
ty * y + c,
|
||||
ty * z + s * x,
|
||||
0,
|
||||
tx * z + s * y,
|
||||
ty * z - s * x,
|
||||
t * z * z + c,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
1,
|
||||
],
|
||||
this[VALUES],
|
||||
)
|
||||
|
||||
if (x !== 0 || y !== 0) {
|
||||
this[IS_2D] = false
|
||||
}
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
skewX(sx) {
|
||||
return newInstance(this[VALUES]).skewXSelf(sx)
|
||||
}
|
||||
|
||||
skewXSelf(sx) {
|
||||
if (typeof sx !== 'number') {
|
||||
return this
|
||||
}
|
||||
|
||||
const t = Math.tan(sx * RAD_PER_DEGREE)
|
||||
|
||||
this[VALUES] = multiply([1, 0, 0, 0, t, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES])
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
skewY(sy) {
|
||||
return newInstance(this[VALUES]).skewYSelf(sy)
|
||||
}
|
||||
|
||||
skewYSelf(sy) {
|
||||
if (typeof sy !== 'number') {
|
||||
return this
|
||||
}
|
||||
|
||||
const t = Math.tan(sy * RAD_PER_DEGREE)
|
||||
|
||||
this[VALUES] = multiply([1, t, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES])
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
flipX() {
|
||||
return newInstance(multiply([-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES]))
|
||||
}
|
||||
|
||||
flipY() {
|
||||
return newInstance(multiply([1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[VALUES]))
|
||||
}
|
||||
|
||||
inverse() {
|
||||
return newInstance(this[VALUES].slice()).invertSelf()
|
||||
}
|
||||
|
||||
invertSelf() {
|
||||
if (this[IS_2D]) {
|
||||
const det = this[VALUES][A] * this[VALUES][D] - this[VALUES][B] * this[VALUES][C]
|
||||
|
||||
// Invertable
|
||||
if (det !== 0) {
|
||||
const newA = this[VALUES][D] / det
|
||||
const newB = -this[VALUES][B] / det
|
||||
const newC = -this[VALUES][C] / det
|
||||
const newD = this[VALUES][A] / det
|
||||
const newE = (this[VALUES][C] * this[VALUES][F] - this[VALUES][D] * this[VALUES][E]) / det
|
||||
const newF = (this[VALUES][B] * this[VALUES][E] - this[VALUES][A] * this[VALUES][F]) / det
|
||||
|
||||
this.a = newA
|
||||
this.b = newB
|
||||
this.c = newC
|
||||
this.d = newD
|
||||
this.e = newE
|
||||
this.f = newF
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
// Not invertable
|
||||
else {
|
||||
this[IS_2D] = false
|
||||
|
||||
this[VALUES] = [NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN, NaN]
|
||||
|
||||
return this
|
||||
}
|
||||
} else {
|
||||
throw new Error('3D matrix inversion is not implemented.')
|
||||
}
|
||||
}
|
||||
|
||||
setMatrixValue(transformList) {
|
||||
const temp = new DOMMatrix(transformList)
|
||||
|
||||
this[VALUES] = temp[VALUES]
|
||||
this[IS_2D] = temp[IS_2D]
|
||||
|
||||
return this
|
||||
}
|
||||
|
||||
transformPoint(point) {
|
||||
const x = point.x || 0
|
||||
const y = point.y || 0
|
||||
const z = point.z || 0
|
||||
const w = point.w || 1
|
||||
|
||||
const values = this[VALUES]
|
||||
|
||||
const nx = values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w
|
||||
const ny = values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w
|
||||
const nz = values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w
|
||||
const nw = values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w
|
||||
|
||||
return new DOMPoint(nx, ny, nz, nw)
|
||||
}
|
||||
|
||||
toFloat32Array() {
|
||||
return Float32Array.from(this[VALUES])
|
||||
}
|
||||
|
||||
toFloat64Array() {
|
||||
return this[VALUES].slice(0)
|
||||
}
|
||||
|
||||
toJSON() {
|
||||
return {
|
||||
a: this.a,
|
||||
b: this.b,
|
||||
c: this.c,
|
||||
d: this.d,
|
||||
e: this.e,
|
||||
f: this.f,
|
||||
m11: this.m11,
|
||||
m12: this.m12,
|
||||
m13: this.m13,
|
||||
m14: this.m14,
|
||||
m21: this.m21,
|
||||
m22: this.m22,
|
||||
m23: this.m23,
|
||||
m24: this.m24,
|
||||
m31: this.m31,
|
||||
m32: this.m32,
|
||||
m33: this.m33,
|
||||
m34: this.m34,
|
||||
m41: this.m41,
|
||||
m42: this.m42,
|
||||
m43: this.m43,
|
||||
m44: this.m44,
|
||||
is2D: this.is2D,
|
||||
isIdentity: this.isIdentity,
|
||||
}
|
||||
}
|
||||
|
||||
toString() {
|
||||
if (this.is2D) {
|
||||
return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`
|
||||
} else {
|
||||
return `matrix3d(${this[VALUES].join(', ')})`
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const propertyName of [
|
||||
'a',
|
||||
'b',
|
||||
'c',
|
||||
'd',
|
||||
'e',
|
||||
'f',
|
||||
'm11',
|
||||
'm12',
|
||||
'm13',
|
||||
'm14',
|
||||
'm21',
|
||||
'm22',
|
||||
'm23',
|
||||
'm24',
|
||||
'm31',
|
||||
'm32',
|
||||
'm33',
|
||||
'm34',
|
||||
'm41',
|
||||
'm42',
|
||||
'm43',
|
||||
'm44',
|
||||
'is2D',
|
||||
'isIdentity',
|
||||
]) {
|
||||
const propertyDescriptor = Object.getOwnPropertyDescriptor(DOMMatrix.prototype, propertyName)
|
||||
propertyDescriptor.enumerable = true
|
||||
Object.defineProperty(DOMMatrix.prototype, propertyName, propertyDescriptor)
|
||||
}
|
||||
|
||||
module.exports = { DOMPoint, DOMMatrix, DOMRect }
|
||||
+1097
File diff suppressed because it is too large
Load Diff
+196
@@ -0,0 +1,196 @@
|
||||
const { platform, homedir } = require('os')
|
||||
const { join } = require('path')
|
||||
|
||||
const {
|
||||
clearAllCache,
|
||||
CanvasRenderingContext2D,
|
||||
CanvasElement,
|
||||
SVGCanvas,
|
||||
Path: Path2D,
|
||||
ImageData,
|
||||
Image,
|
||||
FontKey,
|
||||
GlobalFonts,
|
||||
PathOp,
|
||||
FillType,
|
||||
StrokeJoin,
|
||||
StrokeCap,
|
||||
convertSVGTextToPath,
|
||||
PdfDocument,
|
||||
GifEncoder,
|
||||
GifDisposal,
|
||||
LottieAnimation,
|
||||
} = require('./js-binding')
|
||||
|
||||
const { DOMPoint, DOMMatrix, DOMRect } = require('./geometry')
|
||||
|
||||
const loadImage = require('./load-image')
|
||||
|
||||
// Add Symbol.dispose support for GifEncoder (ECMAScript 2024 Explicit Resource Management)
|
||||
if (GifEncoder && typeof Symbol.dispose !== 'undefined') {
|
||||
GifEncoder.prototype[Symbol.dispose] = function () {
|
||||
this.dispose()
|
||||
}
|
||||
}
|
||||
|
||||
const SvgExportFlag = {
|
||||
ConvertTextToPaths: 0x01,
|
||||
NoPrettyXML: 0x02,
|
||||
RelativePathEncoding: 0x04,
|
||||
}
|
||||
|
||||
if (!('families' in GlobalFonts)) {
|
||||
Object.defineProperty(GlobalFonts, 'families', {
|
||||
get: function () {
|
||||
return JSON.parse(GlobalFonts.getFamilies().toString())
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
if (!('has' in GlobalFonts)) {
|
||||
Object.defineProperty(GlobalFonts, 'has', {
|
||||
value: function has(name) {
|
||||
return !!JSON.parse(GlobalFonts.getFamilies().toString()).find(({ family }) => family === name)
|
||||
},
|
||||
configurable: false,
|
||||
enumerable: false,
|
||||
writable: false,
|
||||
})
|
||||
}
|
||||
|
||||
const _toBlob = CanvasElement.prototype.toBlob
|
||||
const _convertToBlob = CanvasElement.prototype.convertToBlob
|
||||
if ('Blob' in globalThis) {
|
||||
CanvasElement.prototype.toBlob = function toBlob(callback, mimeType, quality) {
|
||||
_toBlob.call(
|
||||
this,
|
||||
function (/** @type {Uint8Array} */ imageBuffer) {
|
||||
const blob = new Blob([imageBuffer.buffer], { type: mimeType })
|
||||
callback(blob)
|
||||
},
|
||||
mimeType,
|
||||
quality,
|
||||
)
|
||||
}
|
||||
CanvasElement.prototype.convertToBlob = function convertToBlob(options) {
|
||||
return _convertToBlob.call(this, options).then((/** @type {Uint8Array} */ imageBuffer) => {
|
||||
const blob = new Blob([imageBuffer.buffer], { type: options?.mime || 'image/png' })
|
||||
return blob
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// oxlint-disable-next-line no-unused-vars
|
||||
CanvasElement.prototype.toBlob = function toBlob(callback, mimeType, quality) {
|
||||
callback(null)
|
||||
}
|
||||
// oxlint-disable-next-line no-unused-vars
|
||||
CanvasElement.prototype.convertToBlob = function convertToBlob(options) {
|
||||
return Promise.reject(new Error('Blob is not supported in this environment'))
|
||||
}
|
||||
}
|
||||
|
||||
const _getTransform = CanvasRenderingContext2D.prototype.getTransform
|
||||
|
||||
CanvasRenderingContext2D.prototype.getTransform = function getTransform() {
|
||||
const transform = _getTransform.apply(this, arguments)
|
||||
// monkey patched, skip
|
||||
if (transform instanceof DOMMatrix) {
|
||||
return transform
|
||||
}
|
||||
const { a, b, c, d, e, f } = transform
|
||||
return new DOMMatrix([a, b, c, d, e, f])
|
||||
}
|
||||
|
||||
// Workaround for webpack bundling issue with drawImage
|
||||
// Store the original drawImage method
|
||||
const _drawImage = CanvasRenderingContext2D.prototype.drawImage
|
||||
|
||||
// Override drawImage to ensure proper type recognition in bundled environments
|
||||
CanvasRenderingContext2D.prototype.drawImage = function drawImage(image, ...args) {
|
||||
// If the image is a Canvas-like object but not recognized due to bundling,
|
||||
// we need to ensure it's properly identified
|
||||
if (image && typeof image === 'object') {
|
||||
// First check if it's a wrapped canvas object
|
||||
if (image.canvas instanceof CanvasElement || image.canvas instanceof SVGCanvas) {
|
||||
image = image.canvas
|
||||
} else if (image._canvas instanceof CanvasElement || image._canvas instanceof SVGCanvas) {
|
||||
image = image._canvas
|
||||
}
|
||||
// Then check if it's a Canvas-like object by checking for getContext method
|
||||
else if (typeof image.getContext === 'function' && image.width && image.height) {
|
||||
// If it has canvas properties but isn't recognized as CanvasElement or SVGCanvas,
|
||||
// try to correct the prototype chain
|
||||
if (!(image instanceof CanvasElement) && !(image instanceof SVGCanvas)) {
|
||||
// Try to create a proper CanvasElement from the canvas-like object
|
||||
// This helps when webpack has transformed the prototype chain
|
||||
Object.setPrototypeOf(image, CanvasElement.prototype)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Call the original drawImage with the potentially corrected image
|
||||
return _drawImage.apply(this, [image, ...args])
|
||||
}
|
||||
|
||||
function createCanvas(width, height, flag) {
|
||||
const isSvgBackend = typeof flag !== 'undefined'
|
||||
return isSvgBackend ? new SVGCanvas(width, height, flag) : new CanvasElement(width, height)
|
||||
}
|
||||
|
||||
class Canvas {
|
||||
constructor(width, height, flag) {
|
||||
return createCanvas(width, height, flag)
|
||||
}
|
||||
|
||||
static [Symbol.hasInstance](instance) {
|
||||
return instance instanceof CanvasElement || instance instanceof SVGCanvas
|
||||
}
|
||||
}
|
||||
|
||||
if (!process.env.DISABLE_SYSTEM_FONTS_LOAD) {
|
||||
GlobalFonts.loadSystemFonts()
|
||||
const platformName = platform()
|
||||
const homedirPath = homedir()
|
||||
switch (platformName) {
|
||||
case 'win32':
|
||||
GlobalFonts.loadFontsFromDir(join(homedirPath, 'AppData', 'Local', 'Microsoft', 'Windows', 'Fonts'))
|
||||
break
|
||||
case 'darwin':
|
||||
GlobalFonts.loadFontsFromDir(join(homedirPath, 'Library', 'Fonts'))
|
||||
break
|
||||
case 'linux':
|
||||
GlobalFonts.loadFontsFromDir(join('usr', 'local', 'share', 'fonts'))
|
||||
GlobalFonts.loadFontsFromDir(join(homedirPath, '.fonts'))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
clearAllCache,
|
||||
Canvas,
|
||||
createCanvas,
|
||||
Path2D,
|
||||
ImageData,
|
||||
Image,
|
||||
PathOp,
|
||||
FillType,
|
||||
StrokeCap,
|
||||
StrokeJoin,
|
||||
SvgExportFlag,
|
||||
GlobalFonts: GlobalFonts,
|
||||
convertSVGTextToPath,
|
||||
DOMPoint,
|
||||
DOMMatrix,
|
||||
DOMRect,
|
||||
loadImage,
|
||||
FontKey,
|
||||
// Export these for better webpack compatibility
|
||||
CanvasElement,
|
||||
SVGCanvas,
|
||||
PDFDocument: PdfDocument,
|
||||
// GIF encoding
|
||||
GifEncoder,
|
||||
GifDisposal,
|
||||
// Lottie animation
|
||||
LottieAnimation,
|
||||
}
|
||||
+464
@@ -0,0 +1,464 @@
|
||||
// prettier-ignore
|
||||
/* eslint-disable */
|
||||
// @ts-nocheck
|
||||
/* auto-generated by NAPI-RS */
|
||||
|
||||
const { readFileSync } = require('fs')
|
||||
let nativeBinding = null
|
||||
const loadErrors = []
|
||||
|
||||
const isMusl = () => {
|
||||
let musl = false
|
||||
if (process.platform === 'linux') {
|
||||
musl = isMuslFromFilesystem()
|
||||
if (musl === null) {
|
||||
musl = isMuslFromReport()
|
||||
}
|
||||
if (musl === null) {
|
||||
musl = isMuslFromChildProcess()
|
||||
}
|
||||
}
|
||||
return musl
|
||||
}
|
||||
|
||||
const isFileMusl = (f) => f.includes('libc.musl-') || f.includes('ld-musl-')
|
||||
|
||||
const isMuslFromFilesystem = () => {
|
||||
try {
|
||||
return readFileSync('/usr/bin/ldd', 'utf-8').includes('musl')
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const isMuslFromReport = () => {
|
||||
let report = null
|
||||
if (typeof process.report?.getReport === 'function') {
|
||||
process.report.excludeNetwork = true
|
||||
report = process.report.getReport()
|
||||
}
|
||||
if (!report) {
|
||||
return null
|
||||
}
|
||||
if (report.header && report.header.glibcVersionRuntime) {
|
||||
return false
|
||||
}
|
||||
if (Array.isArray(report.sharedObjects)) {
|
||||
if (report.sharedObjects.some(isFileMusl)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
const isMuslFromChildProcess = () => {
|
||||
try {
|
||||
return require('child_process').execSync('ldd --version', { encoding: 'utf8' }).includes('musl')
|
||||
} catch (e) {
|
||||
// If we reach this case, we don't know if the system is musl or not, so is better to just fallback to false
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function requireNative() {
|
||||
if (process.env.NAPI_RS_NATIVE_LIBRARY_PATH) {
|
||||
try {
|
||||
return require(process.env.NAPI_RS_NATIVE_LIBRARY_PATH)
|
||||
} catch (err) {
|
||||
loadErrors.push(err)
|
||||
}
|
||||
} else if (process.platform === 'android') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./skia.android-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-android-arm64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./skia.android-arm-eabi.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-android-arm-eabi')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Android ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'win32') {
|
||||
if (process.arch === 'x64') {
|
||||
if (
|
||||
process.config?.variables?.shlib_suffix === 'dll.a' ||
|
||||
process.config?.variables?.node_target_type === 'shared_library'
|
||||
) {
|
||||
try {
|
||||
return require('./skia.win32-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-win32-x64-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./skia.win32-x64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-win32-x64-msvc')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ia32') {
|
||||
try {
|
||||
return require('./skia.win32-ia32-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-win32-ia32-msvc')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./skia.win32-arm64-msvc.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-win32-arm64-msvc')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Windows: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'darwin') {
|
||||
try {
|
||||
return require('./skia.darwin-universal.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-darwin-universal')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./skia.darwin-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-darwin-x64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./skia.darwin-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-darwin-arm64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on macOS: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'freebsd') {
|
||||
if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./skia.freebsd-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-freebsd-x64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./skia.freebsd-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-freebsd-arm64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on FreeBSD: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'linux') {
|
||||
if (process.arch === 'x64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./skia.linux-x64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-x64-musl')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./skia.linux-x64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-x64-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./skia.linux-arm64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-arm64-musl')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./skia.linux-arm64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-arm64-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./skia.linux-arm-musleabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-arm-musleabihf')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./skia.linux-arm-gnueabihf.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-arm-gnueabihf')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'loong64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./skia.linux-loong64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-loong64-musl')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./skia.linux-loong64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-loong64-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'riscv64') {
|
||||
if (isMusl()) {
|
||||
try {
|
||||
return require('./skia.linux-riscv64-musl.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-riscv64-musl')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
return require('./skia.linux-riscv64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-riscv64-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
}
|
||||
} else if (process.arch === 'ppc64') {
|
||||
try {
|
||||
return require('./skia.linux-ppc64-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-ppc64-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 's390x') {
|
||||
try {
|
||||
return require('./skia.linux-s390x-gnu.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-linux-s390x-gnu')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on Linux: ${process.arch}`))
|
||||
}
|
||||
} else if (process.platform === 'openharmony') {
|
||||
if (process.arch === 'arm64') {
|
||||
try {
|
||||
return require('./skia.openharmony-arm64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-openharmony-arm64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'x64') {
|
||||
try {
|
||||
return require('./skia.openharmony-x64.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
return require('@napi-rs/canvas-openharmony-x64')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else if (process.arch === 'arm') {
|
||||
try {
|
||||
return require('./skia.openharmony-arm.node')
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
try {
|
||||
const binding = require('@napi-rs/canvas-openharmony-arm')
|
||||
return binding
|
||||
} catch (e) {
|
||||
loadErrors.push(e)
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported architecture on OpenHarmony: ${process.arch}`))
|
||||
}
|
||||
} else {
|
||||
loadErrors.push(new Error(`Unsupported OS: ${process.platform}, architecture: ${process.arch}`))
|
||||
}
|
||||
}
|
||||
|
||||
nativeBinding = requireNative()
|
||||
|
||||
if (!nativeBinding || process.env.NAPI_RS_FORCE_WASI) {
|
||||
let wasiBinding = null
|
||||
let wasiBindingError = null
|
||||
try {
|
||||
wasiBinding = require('./skia.wasi.cjs')
|
||||
nativeBinding = wasiBinding
|
||||
} catch (err) {
|
||||
if (process.env.NAPI_RS_FORCE_WASI) {
|
||||
wasiBindingError = err
|
||||
}
|
||||
}
|
||||
if (!nativeBinding) {
|
||||
try {
|
||||
wasiBinding = require('@napi-rs/canvas-wasm32-wasi')
|
||||
nativeBinding = wasiBinding
|
||||
} catch (err) {
|
||||
if (process.env.NAPI_RS_FORCE_WASI) {
|
||||
wasiBindingError.cause = err
|
||||
loadErrors.push(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!nativeBinding) {
|
||||
if (loadErrors.length > 0) {
|
||||
throw new Error(
|
||||
`Cannot find native binding. ` +
|
||||
`npm has a bug related to optional dependencies (https://github.com/npm/cli/issues/4828). ` +
|
||||
'Please try `npm i` again after removing both package-lock.json and node_modules directory.',
|
||||
{
|
||||
cause: loadErrors.reduce((err, cur) => {
|
||||
cur.cause = err
|
||||
return cur
|
||||
}),
|
||||
},
|
||||
)
|
||||
}
|
||||
throw new Error(`Failed to load native binding`)
|
||||
}
|
||||
|
||||
module.exports = nativeBinding
|
||||
module.exports.GlobalFonts = nativeBinding.GlobalFonts
|
||||
module.exports.CanvasElement = nativeBinding.CanvasElement
|
||||
module.exports.CanvasGradient = nativeBinding.CanvasGradient
|
||||
module.exports.CanvasPattern = nativeBinding.CanvasPattern
|
||||
module.exports.CanvasRenderingContext2D = nativeBinding.CanvasRenderingContext2D
|
||||
module.exports.FontKey = nativeBinding.FontKey
|
||||
module.exports.Image = nativeBinding.Image
|
||||
module.exports.ImageData = nativeBinding.ImageData
|
||||
module.exports.Path = nativeBinding.Path
|
||||
module.exports.PdfDocument = nativeBinding.PdfDocument
|
||||
module.exports.SVGCanvas = nativeBinding.SVGCanvas
|
||||
module.exports.ChromaSubsampling = nativeBinding.ChromaSubsampling
|
||||
module.exports.clearAllCache = nativeBinding.clearAllCache
|
||||
module.exports.convertSVGTextToPath = nativeBinding.convertSVGTextToPath
|
||||
module.exports.FillType = nativeBinding.FillType
|
||||
module.exports.PathOp = nativeBinding.PathOp
|
||||
module.exports.StrokeCap = nativeBinding.StrokeCap
|
||||
module.exports.StrokeJoin = nativeBinding.StrokeJoin
|
||||
module.exports.SvgExportFlag = nativeBinding.SvgExportFlag
|
||||
module.exports.GifEncoder = nativeBinding.GifEncoder
|
||||
module.exports.GifDisposal = nativeBinding.GifDisposal
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
const fs = require('fs')
|
||||
const { Readable } = require('stream')
|
||||
const { URL } = require('url')
|
||||
|
||||
const { Image } = require('./js-binding')
|
||||
|
||||
let http, https
|
||||
|
||||
const MAX_REDIRECTS = 20
|
||||
const REDIRECT_STATUSES = new Set([301, 302])
|
||||
|
||||
/**
|
||||
* Loads the given source into canvas Image
|
||||
* @param {string|URL|Image|Buffer} source The image source to be loaded
|
||||
* @param {object} options Options passed to the loader
|
||||
*/
|
||||
module.exports = async function loadImage(source, options = {}) {
|
||||
// use the same buffer without copying if the source is a buffer
|
||||
if (Buffer.isBuffer(source) || source instanceof Uint8Array) return createImage(source, options.alt)
|
||||
// load readable stream as image
|
||||
if (source instanceof Readable) return createImage(await consumeStream(source), options.alt)
|
||||
// construct a Uint8Array if the source is ArrayBuffer or SharedArrayBuffer
|
||||
if (source instanceof ArrayBuffer || source instanceof SharedArrayBuffer)
|
||||
return createImage(new Uint8Array(source), options.alt)
|
||||
// construct a buffer if the source is buffer-like
|
||||
if (isBufferLike(source)) return createImage(Buffer.from(source), options.alt)
|
||||
// if the source is Image instance, copy the image src to new image
|
||||
if (source instanceof Image) return createImage(source.src, options.alt)
|
||||
// if source is string and in data uri format, construct image using data uri
|
||||
if (typeof source === 'string' && source.trimStart().startsWith('data:')) {
|
||||
const commaIdx = source.indexOf(',')
|
||||
const encoding = source.lastIndexOf('base64', commaIdx) < 0 ? 'utf-8' : 'base64'
|
||||
const data = Buffer.from(source.slice(commaIdx + 1), encoding)
|
||||
// Empty payload would silently complete on Image (HTML spec) and the loadImage
|
||||
// promise would never settle; reject upfront instead.
|
||||
// See https://github.com/Brooooooklyn/canvas/issues/1255
|
||||
if (data.length === 0) throw new Error(`Invalid data URI: empty payload in ${source.slice(0, 64)}`)
|
||||
return createImage(data, options.alt)
|
||||
}
|
||||
// if source is a string or URL instance
|
||||
if (typeof source === 'string') {
|
||||
// if the source exists as a file, construct image from that file
|
||||
if (!source.startsWith('http') && !source.startsWith('https') && (await exists(source))) {
|
||||
return createImage(source, options.alt)
|
||||
} else {
|
||||
// the source is a remote url here
|
||||
source = new URL(source)
|
||||
// attempt to download the remote source and construct image
|
||||
const data = await new Promise((resolve, reject) =>
|
||||
makeRequest(
|
||||
source,
|
||||
resolve,
|
||||
reject,
|
||||
typeof options.maxRedirects === 'number' && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS,
|
||||
options.requestOptions,
|
||||
),
|
||||
)
|
||||
return createImage(data, options.alt)
|
||||
}
|
||||
}
|
||||
|
||||
if (source instanceof URL) {
|
||||
if (source.protocol === 'file:') {
|
||||
// remove the leading slash on windows
|
||||
return createImage(process.platform === 'win32' ? source.pathname.substring(1) : source.pathname, options.alt)
|
||||
} else {
|
||||
const data = await new Promise((resolve, reject) =>
|
||||
makeRequest(
|
||||
source,
|
||||
resolve,
|
||||
reject,
|
||||
typeof options.maxRedirects === 'number' && options.maxRedirects >= 0 ? options.maxRedirects : MAX_REDIRECTS,
|
||||
options.requestOptions,
|
||||
),
|
||||
)
|
||||
return createImage(data, options.alt)
|
||||
}
|
||||
}
|
||||
|
||||
// throw error as don't support that source
|
||||
throw new TypeError('unsupported image source')
|
||||
}
|
||||
|
||||
function makeRequest(url, resolve, reject, redirectCount, requestOptions) {
|
||||
const isHttps = url.protocol === 'https:'
|
||||
// lazy load the lib
|
||||
const lib = isHttps ? (!https ? (https = require('https')) : https) : !http ? (http = require('http')) : http
|
||||
|
||||
lib
|
||||
.get(url.toString(), requestOptions || {}, (res) => {
|
||||
try {
|
||||
const shouldRedirect = REDIRECT_STATUSES.has(res.statusCode) && typeof res.headers.location === 'string'
|
||||
if (shouldRedirect && redirectCount > 0)
|
||||
return makeRequest(
|
||||
new URL(res.headers.location, url.origin),
|
||||
resolve,
|
||||
reject,
|
||||
redirectCount - 1,
|
||||
requestOptions,
|
||||
)
|
||||
if (typeof res.statusCode === 'number' && (res.statusCode < 200 || res.statusCode >= 300)) {
|
||||
return reject(new Error(`remote source rejected with status code ${res.statusCode}`))
|
||||
}
|
||||
|
||||
consumeStream(res).then(resolve, reject)
|
||||
} catch (err) {
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
.on('error', reject)
|
||||
}
|
||||
|
||||
// use stream/consumers in the future?
|
||||
function consumeStream(res) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const chunks = []
|
||||
|
||||
res.on('data', (chunk) => chunks.push(chunk))
|
||||
res.on('end', () => resolve(Buffer.concat(chunks)))
|
||||
res.on('error', reject)
|
||||
})
|
||||
}
|
||||
|
||||
async function createImage(src, alt) {
|
||||
// Empty Buffer / Uint8Array: Image follows the HTML spec (silent completion,
|
||||
// no events) so the promise below would hang. Reject upfront.
|
||||
// See https://github.com/Brooooooklyn/canvas/issues/1255
|
||||
if ((Buffer.isBuffer(src) || src instanceof Uint8Array) && src.length === 0) {
|
||||
throw new Error('loadImage: empty image data')
|
||||
}
|
||||
|
||||
const image = new Image()
|
||||
if (typeof alt === 'string') image.alt = alt
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
image.onload = () => {
|
||||
// Wait for bitmap decode before resolving
|
||||
image.decode().then(() => resolve(image), reject)
|
||||
}
|
||||
image.onerror = (e) => reject(e)
|
||||
image.src = src
|
||||
})
|
||||
}
|
||||
|
||||
function isBufferLike(src) {
|
||||
return (src && src.type === 'Buffer') || Array.isArray(src)
|
||||
}
|
||||
|
||||
async function exists(path) {
|
||||
try {
|
||||
await fs.promises.access(path, fs.constants.F_OK)
|
||||
return true
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
+216
@@ -0,0 +1,216 @@
|
||||
/**
|
||||
* node-canvas compatibility layer for @napi-rs/canvas.
|
||||
*
|
||||
* Provides a drop-in replacement API matching the `canvas` (node-canvas) npm package.
|
||||
* Quality values for JPEG/WebP use the node-canvas 0-1 scale and are automatically
|
||||
* converted to @napi-rs/canvas's 0-100 scale internally.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Drop-in replacement: change this import
|
||||
* // import { createCanvas, registerFont, loadImage } from 'canvas'
|
||||
* // to this:
|
||||
* import { createCanvas, registerFont, loadImage } from '@napi-rs/canvas/node-canvas'
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { Readable } from 'node:stream'
|
||||
|
||||
// Re-export types from @napi-rs/canvas that are API-compatible
|
||||
import {
|
||||
Image as NapiImage,
|
||||
ImageData as NapiImageData,
|
||||
Path2D as NapiPath2D,
|
||||
DOMPoint as NapiDOMPoint,
|
||||
DOMMatrix as NapiDOMMatrix,
|
||||
DOMRect as NapiDOMRect,
|
||||
SKRSContext2D,
|
||||
Canvas as NapiCanvas,
|
||||
SvgCanvas,
|
||||
SvgExportFlag,
|
||||
IGlobalFonts,
|
||||
PDFDocument,
|
||||
} from './index'
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Config interfaces (node-canvas conventions)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface PngConfig {
|
||||
/** ZLIB compression level (0-9). Accepted for compatibility; Skia uses its own encoder. */
|
||||
compressionLevel?: 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
|
||||
/** PNG filter flags. Accepted for compatibility; Skia uses its own encoder. */
|
||||
filters?: number
|
||||
/** Palette for indexed PNGs. Not supported by Skia backend. */
|
||||
palette?: Uint8ClampedArray
|
||||
/** Background color index for indexed PNGs. Not supported by Skia backend. */
|
||||
backgroundIndex?: number
|
||||
/** Pixels per inch. Accepted for compatibility. */
|
||||
resolution?: number
|
||||
}
|
||||
|
||||
export interface JpegConfig {
|
||||
/** Quality between 0 and 1. Defaults to 0.75. Converted to 0-100 for Skia. */
|
||||
quality?: number
|
||||
/** Progressive encoding. Accepted for compatibility; not supported by Skia backend. */
|
||||
progressive?: boolean
|
||||
/** 2x2 chroma subsampling. Accepted for compatibility; not supported by Skia backend. */
|
||||
chromaSubsampling?: boolean
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Canvas class (with node-canvas compatible methods)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface Canvas extends Omit<NapiCanvas, 'toBuffer' | 'toDataURL'> {
|
||||
/**
|
||||
* Encode the canvas as a PNG and return a Node.js Readable stream.
|
||||
* @param config PNG encoding options (accepted for compatibility)
|
||||
*/
|
||||
createPNGStream(config?: PngConfig): PNGStream
|
||||
|
||||
/**
|
||||
* Encode the canvas as a JPEG and return a Node.js Readable stream.
|
||||
* @param config JPEG encoding options. Quality is 0-1 (default 0.75).
|
||||
*/
|
||||
createJPEGStream(config?: JpegConfig): JPEGStream
|
||||
|
||||
// --- toBuffer overloads (node-canvas conventions) ---
|
||||
|
||||
/** Encode as PNG (default). */
|
||||
toBuffer(): Buffer
|
||||
toBuffer(mimeType: 'image/png', config?: PngConfig): Buffer
|
||||
/** Encode as JPEG. Quality in config is 0-1 (default 0.75). */
|
||||
toBuffer(mimeType: 'image/jpeg', config?: JpegConfig): Buffer
|
||||
/** Get raw unencoded pixel data. */
|
||||
toBuffer(mimeType: 'raw'): Buffer
|
||||
|
||||
/** Async: encode as PNG (default) via callback. */
|
||||
toBuffer(cb: (err: Error | null, result: Buffer) => void): void
|
||||
toBuffer(cb: (err: Error | null, result: Buffer) => void, mimeType: 'image/png', config?: PngConfig): void
|
||||
/** Async: encode as JPEG via callback. Quality in config is 0-1 (default 0.75). */
|
||||
toBuffer(cb: (err: Error | null, result: Buffer) => void, mimeType: 'image/jpeg', config?: JpegConfig): void
|
||||
|
||||
// --- toDataURL overloads (node-canvas conventions) ---
|
||||
|
||||
/** Encode as PNG data URL (default). */
|
||||
toDataURL(): string
|
||||
toDataURL(mimeType: 'image/png'): string
|
||||
/** Encode as JPEG data URL. Quality is 0-1. */
|
||||
toDataURL(mimeType: 'image/jpeg', quality?: number): string
|
||||
/** Async: encode as data URL via callback. */
|
||||
toDataURL(cb: (err: Error | null, result: string) => void): void
|
||||
toDataURL(mimeType: 'image/png', cb: (err: Error | null, result: string) => void): void
|
||||
toDataURL(mimeType: 'image/jpeg', cb: (err: Error | null, result: string) => void): void
|
||||
toDataURL(mimeType: 'image/jpeg', quality: number, cb: (err: Error | null, result: string) => void): void
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stream classes
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Readable stream that emits PNG-encoded data. */
|
||||
export class PNGStream extends Readable {}
|
||||
|
||||
/** Readable stream that emits JPEG-encoded data. */
|
||||
export class JPEGStream extends Readable {}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Factory functions
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new canvas with node-canvas compatible API.
|
||||
* Canvas instances created through this function have `createPNGStream()`,
|
||||
* `createJPEGStream()`, and node-canvas compatible `toBuffer()` overloads.
|
||||
*
|
||||
* When `type` is `'svg'`, returns a `SvgCanvas` which produces vector output
|
||||
* via `getContent()` instead of raster encoding methods.
|
||||
*
|
||||
* @param width Canvas width in pixels
|
||||
* @param height Canvas height in pixels
|
||||
* @param type Canvas type: 'image' (default) or 'svg'
|
||||
*/
|
||||
export function createCanvas(width: number, height: number, type: 'svg'): SvgCanvas
|
||||
export function createCanvas(width: number, height: number, type?: 'image'): Canvas
|
||||
|
||||
/**
|
||||
* Create an ImageData instance.
|
||||
* @param data Pixel data array
|
||||
* @param width Width in pixels
|
||||
* @param height Height in pixels (calculated from data length if omitted)
|
||||
*/
|
||||
export function createImageData(data: Uint8ClampedArray, width: number, height?: number): NapiImageData
|
||||
export function createImageData(width: number, height: number): NapiImageData
|
||||
|
||||
/**
|
||||
* Load an image from a file path, URL, Buffer, or other source.
|
||||
* Returns a Promise that resolves to an Image instance.
|
||||
*/
|
||||
export function loadImage(
|
||||
source: string | URL | Buffer | ArrayBufferLike | Uint8Array | NapiImage | import('stream').Readable,
|
||||
options?: { alt?: string; maxRedirects?: number; requestOptions?: import('http').RequestOptions },
|
||||
): Promise<NapiImage>
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Font registration (node-canvas convention)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Register a font file for use in canvas text rendering.
|
||||
* Compatible with node-canvas's `registerFont()`.
|
||||
*
|
||||
* Note: @napi-rs/canvas auto-detects weight and style from font file metadata
|
||||
* (matching browser behavior). The `weight` and `style` properties in fontFace
|
||||
* are accepted for API compatibility.
|
||||
*
|
||||
* @param path Absolute path to the font file (.ttf, .otf, etc.)
|
||||
* @param fontFace Font face properties matching CSS @font-face descriptors
|
||||
*/
|
||||
export function registerFont(path: string, fontFace: { family: string; weight?: string; style?: string }): void
|
||||
|
||||
/**
|
||||
* Deregister all previously registered fonts.
|
||||
* Compatible with node-canvas's `deregisterAllFonts()`.
|
||||
*/
|
||||
export function deregisterAllFonts(): void
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Re-exported classes & values
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Canvas is exported both as a type (the compat interface above) and as a
|
||||
// value (constructor that returns compat Canvas instances at runtime).
|
||||
export declare const Canvas: {
|
||||
new (width: number, height: number): Canvas
|
||||
new (width: number, height: number, flag: SvgExportFlag): SvgCanvas
|
||||
prototype: NapiCanvas
|
||||
}
|
||||
|
||||
export { NapiImage as Image }
|
||||
export { NapiImageData as ImageData }
|
||||
export { NapiPath2D as Path2D }
|
||||
export { NapiDOMPoint as DOMPoint }
|
||||
export { NapiDOMMatrix as DOMMatrix }
|
||||
export { NapiDOMRect as DOMRect }
|
||||
|
||||
// SKRSContext2D is an interface in index.d.ts but a class value at runtime.
|
||||
// Declare as const to export the value for instanceof checks and re-export the type.
|
||||
export type CanvasRenderingContext2D = SKRSContext2D
|
||||
export declare const CanvasRenderingContext2D: { prototype: SKRSContext2D }
|
||||
/** Legacy alias for CanvasRenderingContext2D. */
|
||||
export type Context2d = SKRSContext2D
|
||||
export declare const Context2d: { prototype: SKRSContext2D }
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// @napi-rs/canvas extras (not part of node-canvas, but useful)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export { IGlobalFonts }
|
||||
export declare const GlobalFonts: IGlobalFonts & {
|
||||
/** Reload system fonts. Available at runtime but not declared in IGlobalFonts. */
|
||||
loadSystemFonts(): number
|
||||
}
|
||||
export { PDFDocument }
|
||||
export { NapiCanvas as CanvasElement }
|
||||
export { SvgCanvas as SVGCanvas }
|
||||
+379
@@ -0,0 +1,379 @@
|
||||
'use strict'
|
||||
|
||||
const { Readable } = require('node:stream')
|
||||
|
||||
const {
|
||||
createCanvas: _createCanvas,
|
||||
Canvas,
|
||||
CanvasElement,
|
||||
SVGCanvas,
|
||||
GlobalFonts,
|
||||
Image,
|
||||
ImageData,
|
||||
Path2D,
|
||||
DOMPoint,
|
||||
DOMMatrix,
|
||||
DOMRect,
|
||||
loadImage,
|
||||
PDFDocument,
|
||||
SvgExportFlag,
|
||||
} = require('./index.js')
|
||||
|
||||
// CanvasRenderingContext2D is not re-exported by index.js, grab from native bindings
|
||||
const { CanvasRenderingContext2D } = require('./js-binding')
|
||||
|
||||
// node-canvas defaults JPEG quality to 0.75 across all paths.
|
||||
// @napi-rs/canvas native default is 0.92 (matching Blink/Chrome).
|
||||
const NODE_CANVAS_DEFAULT_QUALITY = 0.75
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stream classes (node-canvas returns Node.js Readable streams)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
class PNGStream extends Readable {
|
||||
constructor(canvas, options) {
|
||||
super()
|
||||
this._canvas = canvas
|
||||
this._options = options || {}
|
||||
this._done = false
|
||||
}
|
||||
|
||||
_read() {
|
||||
if (this._done) return
|
||||
this._done = true
|
||||
this._canvas
|
||||
.encode('png')
|
||||
.then((buf) => {
|
||||
this.push(buf)
|
||||
this.push(null)
|
||||
})
|
||||
.catch((err) => {
|
||||
this.destroy(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
class JPEGStream extends Readable {
|
||||
constructor(canvas, options) {
|
||||
super()
|
||||
this._canvas = canvas
|
||||
const opts = options || {}
|
||||
// node-canvas quality: 0-1 (default 0.75), encode() expects 0-100
|
||||
this._quality = Math.round((opts.quality != null ? opts.quality : NODE_CANVAS_DEFAULT_QUALITY) * 100)
|
||||
this._done = false
|
||||
}
|
||||
|
||||
_read() {
|
||||
if (this._done) return
|
||||
this._done = true
|
||||
this._canvas
|
||||
.encode('jpeg', this._quality)
|
||||
.then((buf) => {
|
||||
this.push(buf)
|
||||
this.push(null)
|
||||
})
|
||||
.catch((err) => {
|
||||
this.destroy(err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Quality normalization helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const MIME_FORMAT_MAP = {
|
||||
'image/png': 'png',
|
||||
'image/jpeg': 'jpeg',
|
||||
'image/webp': 'webp',
|
||||
'image/avif': 'avif',
|
||||
'image/gif': 'gif',
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract quality from a node-canvas config object or number.
|
||||
* Returns the raw 0-1 quality value, defaulting to NODE_CANVAS_DEFAULT_QUALITY
|
||||
* for JPEG/WebP when not specified.
|
||||
*
|
||||
* @param {string} mime
|
||||
* @param {number|object|undefined} configOrQuality
|
||||
* @returns {number|undefined} 0-1 scale for JPEG/WebP, undefined for other mimes
|
||||
*/
|
||||
function _extractQuality(mime, configOrQuality) {
|
||||
if (mime !== 'image/jpeg' && mime !== 'image/webp') return undefined
|
||||
if (configOrQuality == null) return NODE_CANVAS_DEFAULT_QUALITY
|
||||
if (typeof configOrQuality === 'number') return configOrQuality
|
||||
if (typeof configOrQuality === 'object' && configOrQuality.quality != null) {
|
||||
return configOrQuality.quality
|
||||
}
|
||||
return NODE_CANVAS_DEFAULT_QUALITY
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Compat methods added to each canvas instance created via this module
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Keep references to the original prototype methods
|
||||
const _origToBuffer = CanvasElement.prototype.toBuffer
|
||||
const _origToDataURL = CanvasElement.prototype.toDataURL
|
||||
|
||||
function _compatCreatePNGStream(options) {
|
||||
return new PNGStream(this, options)
|
||||
}
|
||||
|
||||
function _compatCreateJPEGStream(options) {
|
||||
return new JPEGStream(this, options)
|
||||
}
|
||||
|
||||
/**
|
||||
* node-canvas compatible toBuffer:
|
||||
* - toBuffer() → PNG (sync)
|
||||
* - toBuffer('image/png', config?) → PNG (sync)
|
||||
* - toBuffer('image/jpeg', config?) → JPEG (sync, quality 0-1)
|
||||
* - toBuffer('raw') → raw pixel data (sync)
|
||||
* - toBuffer(callback) → PNG (async)
|
||||
* - toBuffer(callback, mime, config?) → specified format (async)
|
||||
*/
|
||||
function _compatToBuffer(mimeOrCallback, configOrQuality) {
|
||||
// Callback form: toBuffer(callback) or toBuffer(callback, mime, config)
|
||||
if (typeof mimeOrCallback === 'function') {
|
||||
const callback = mimeOrCallback
|
||||
const mime = typeof configOrQuality === 'string' ? configOrQuality : 'image/png'
|
||||
const config = arguments[2]
|
||||
|
||||
if (mime === 'raw') {
|
||||
let buf
|
||||
try {
|
||||
buf = this.data()
|
||||
} catch (err) {
|
||||
callback(err)
|
||||
return
|
||||
}
|
||||
callback(null, buf)
|
||||
return
|
||||
}
|
||||
|
||||
const format = MIME_FORMAT_MAP[mime]
|
||||
if (!format) {
|
||||
callback(new TypeError(`Unsupported MIME type "${mime}". Supported: ${Object.keys(MIME_FORMAT_MAP).join(', ')}`))
|
||||
return
|
||||
}
|
||||
const q = _extractQuality(mime, config)
|
||||
this.encode(format, q != null ? Math.round(q * 100) : undefined).then(
|
||||
(buf) => callback(null, buf),
|
||||
(err) => callback(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync: no args → PNG
|
||||
if (mimeOrCallback === undefined) {
|
||||
return _origToBuffer.call(this, 'image/png')
|
||||
}
|
||||
|
||||
// Sync: raw pixel data
|
||||
if (mimeOrCallback === 'raw') {
|
||||
return this.data()
|
||||
}
|
||||
|
||||
// Sync: extract quality (0-1) and scale to 0-100 for native toBuffer
|
||||
const q = _extractQuality(mimeOrCallback, configOrQuality)
|
||||
return _origToBuffer.call(this, mimeOrCallback, q != null ? Math.round(q * 100) : undefined)
|
||||
}
|
||||
|
||||
/**
|
||||
* node-canvas compatible toDataURL:
|
||||
* - toDataURL() → PNG data URL (sync)
|
||||
* - toDataURL('image/jpeg', quality) → JPEG data URL, quality 0-1 (sync)
|
||||
* - toDataURL(callback) → PNG data URL (async)
|
||||
* - toDataURL(mime, callback) → data URL (async)
|
||||
* - toDataURL(mime, quality, callback) → data URL with quality (async)
|
||||
*/
|
||||
function _compatToDataURL(mimeOrCallback, qualityOrCallback) {
|
||||
// toDataURL(callback)
|
||||
if (typeof mimeOrCallback === 'function') {
|
||||
this.toDataURLAsync('image/png').then(
|
||||
(url) => mimeOrCallback(null, url),
|
||||
(err) => mimeOrCallback(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
// toDataURL(mime, callback)
|
||||
if (typeof qualityOrCallback === 'function') {
|
||||
this.toDataURLAsync(mimeOrCallback, _extractQuality(mimeOrCallback, undefined)).then(
|
||||
(url) => qualityOrCallback(null, url),
|
||||
(err) => qualityOrCallback(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
// toDataURL(mime, quality, callback)
|
||||
const cb = arguments[2]
|
||||
if (typeof cb === 'function') {
|
||||
// Native toDataURLAsync expects 0-1 (f64) — pass quality through as-is
|
||||
const quality = _extractQuality(mimeOrCallback, qualityOrCallback)
|
||||
this.toDataURLAsync(mimeOrCallback, quality).then(
|
||||
(url) => cb(null, url),
|
||||
(err) => cb(err),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Sync form: native toDataURL expects 0-1 (f64) — pass quality through as-is
|
||||
const quality = _extractQuality(mimeOrCallback, qualityOrCallback)
|
||||
return _origToDataURL.call(this, mimeOrCallback, quality)
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach node-canvas compatible methods to a CanvasElement instance.
|
||||
*/
|
||||
function _addCompatMethods(canvas) {
|
||||
canvas.createPNGStream = _compatCreatePNGStream
|
||||
canvas.createJPEGStream = _compatCreateJPEGStream
|
||||
canvas.toBuffer = _compatToBuffer
|
||||
canvas.toDataURL = _compatToDataURL
|
||||
return canvas
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API: registerFont / deregisterAllFonts
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Track FontKeys from registerFont() so deregisterAllFonts() can remove
|
||||
// only user-registered fonts (matching node-canvas behavior which leaves
|
||||
// system fonts untouched).
|
||||
const _registeredFontKeys = []
|
||||
|
||||
/**
|
||||
* Register a font file with the specified font face properties.
|
||||
* Compatible with node-canvas's registerFont(path, { family, weight?, style? }).
|
||||
*
|
||||
* Note: @napi-rs/canvas auto-detects weight and style from font file metadata
|
||||
* (matching browser behavior). The weight and style properties in fontFace are
|
||||
* accepted for API compatibility but the actual values are read from the font.
|
||||
*
|
||||
* @param {string} path Absolute path to the font file (.ttf, .otf, etc.)
|
||||
* @param {{ family: string, weight?: string, style?: string }} fontFace
|
||||
*/
|
||||
function registerFont(path, fontFace) {
|
||||
if (!fontFace || typeof fontFace.family !== 'string') {
|
||||
throw new TypeError('registerFont requires a fontFace with a "family" property')
|
||||
}
|
||||
const key = GlobalFonts.registerFromPath(path, fontFace.family)
|
||||
if (!key) {
|
||||
throw new Error(`Failed to register font from "${path}" with family "${fontFace.family}"`)
|
||||
}
|
||||
_registeredFontKeys.push(key)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deregister all fonts previously registered via registerFont().
|
||||
* Compatible with node-canvas's deregisterAllFonts() which only removes
|
||||
* user-registered fonts and leaves system fonts untouched.
|
||||
*/
|
||||
function deregisterAllFonts() {
|
||||
if (_registeredFontKeys.length > 0) {
|
||||
GlobalFonts.removeBatch(_registeredFontKeys)
|
||||
_registeredFontKeys.length = 0
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API: createCanvas / createImageData
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Create a new canvas instance with node-canvas compatible API.
|
||||
*
|
||||
* @param {number} width
|
||||
* @param {number} height
|
||||
* @param {'image'|'svg'} [type='image']
|
||||
* @returns {Canvas}
|
||||
*/
|
||||
function createCanvas(width, height, type) {
|
||||
if (type === 'svg') {
|
||||
// SvgExportFlag enum requires a valid variant; NoPrettyXML (0x02) is the
|
||||
// least impactful on rendering behavior (only affects XML whitespace).
|
||||
return _createCanvas(width, height, SvgExportFlag.NoPrettyXML)
|
||||
}
|
||||
if (type === 'pdf') {
|
||||
throw new Error(
|
||||
'createCanvas with type "pdf" is not supported. Use the PDFDocument class from @napi-rs/canvas directly.',
|
||||
)
|
||||
}
|
||||
if (type != null && type !== 'image') {
|
||||
throw new TypeError(`createCanvas: unknown type "${type}". Supported types: "image", "svg".`)
|
||||
}
|
||||
return _addCompatMethods(_createCanvas(width, height))
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an ImageData instance.
|
||||
* Compatible with node-canvas's createImageData().
|
||||
*
|
||||
* @param {Uint8ClampedArray|number} dataOrWidth
|
||||
* @param {number} widthOrHeight
|
||||
* @param {number} [height]
|
||||
* @returns {ImageData}
|
||||
*/
|
||||
function createImageData(dataOrWidth, widthOrHeight, height) {
|
||||
if (typeof dataOrWidth === 'number') {
|
||||
return new ImageData(dataOrWidth, widthOrHeight)
|
||||
}
|
||||
if (height != null) {
|
||||
return new ImageData(dataOrWidth, widthOrHeight, height)
|
||||
}
|
||||
return new ImageData(dataOrWidth, widthOrHeight)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Canvas constructor wrapper
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// In node-canvas, `new Canvas(w, h)` and `createCanvas(w, h)` produce
|
||||
// equivalent canvases. Wrap the native Canvas constructor so that
|
||||
// `new Canvas(w, h)` also gets compat methods, without polluting the
|
||||
// prototype (which would affect @napi-rs/canvas users who don't use
|
||||
// this compat layer).
|
||||
const CompatCanvas = new Proxy(Canvas, {
|
||||
construct(target, args) {
|
||||
const canvas = new target(...args)
|
||||
if (canvas instanceof CanvasElement) {
|
||||
return _addCompatMethods(canvas)
|
||||
}
|
||||
return canvas
|
||||
},
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Exports (matches node-canvas export shape)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
module.exports = {
|
||||
// Factory functions
|
||||
Canvas: CompatCanvas,
|
||||
createCanvas,
|
||||
createImageData,
|
||||
loadImage,
|
||||
registerFont,
|
||||
deregisterAllFonts,
|
||||
|
||||
// Classes
|
||||
Image,
|
||||
ImageData,
|
||||
CanvasRenderingContext2D,
|
||||
Context2d: CanvasRenderingContext2D,
|
||||
PNGStream,
|
||||
JPEGStream,
|
||||
Path2D,
|
||||
|
||||
// Geometry
|
||||
DOMPoint,
|
||||
DOMMatrix,
|
||||
DOMRect,
|
||||
|
||||
// @napi-rs/canvas extras (available but not part of node-canvas)
|
||||
GlobalFonts,
|
||||
PDFDocument,
|
||||
CanvasElement,
|
||||
SVGCanvas,
|
||||
}
|
||||
+176
@@ -0,0 +1,176 @@
|
||||
{
|
||||
"name": "@napi-rs/canvas",
|
||||
"version": "0.1.100",
|
||||
"description": "Canvas for Node.js with skia backend",
|
||||
"main": "index.js",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/Brooooooklyn/canvas.git"
|
||||
},
|
||||
"workspaces": [
|
||||
"e2e/*"
|
||||
],
|
||||
"license": "MIT",
|
||||
"keywords": [
|
||||
"napi-rs",
|
||||
"NAPI",
|
||||
"N-API",
|
||||
"Rust",
|
||||
"node-addon",
|
||||
"node-addon-api",
|
||||
"canvas",
|
||||
"image",
|
||||
"pdf",
|
||||
"svg",
|
||||
"skia"
|
||||
],
|
||||
"files": [
|
||||
"index.d.ts",
|
||||
"index.js",
|
||||
"geometry.js",
|
||||
"js-binding.js",
|
||||
"load-image.js",
|
||||
"node-canvas.js",
|
||||
"node-canvas.d.ts"
|
||||
],
|
||||
"napi": {
|
||||
"binaryName": "skia",
|
||||
"targets": [
|
||||
"x86_64-unknown-linux-gnu",
|
||||
"x86_64-apple-darwin",
|
||||
"x86_64-pc-windows-msvc",
|
||||
"aarch64-pc-windows-msvc",
|
||||
"armv7-unknown-linux-gnueabihf",
|
||||
"x86_64-unknown-linux-musl",
|
||||
"aarch64-unknown-linux-gnu",
|
||||
"aarch64-unknown-linux-musl",
|
||||
"aarch64-apple-darwin",
|
||||
"aarch64-linux-android",
|
||||
"riscv64-unknown-linux-gnu"
|
||||
]
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10"
|
||||
},
|
||||
"publishConfig": {
|
||||
"registry": "https://registry.npmjs.org/",
|
||||
"access": "public"
|
||||
},
|
||||
"scripts": {
|
||||
"artifacts": "napi artifacts",
|
||||
"bench": "node --import @oxc-node/core/register benchmark/bench.ts",
|
||||
"build": "napi build --platform --release --js js-binding.js",
|
||||
"build:debug": "napi build --platform --js js-binding.js",
|
||||
"example-lottie": "yarn oxnode ./example/lottie-to-video.ts",
|
||||
"format": "run-p format:source format:rs format:toml",
|
||||
"format:rs": "cargo fmt",
|
||||
"format:source": "prettier . -w",
|
||||
"format:toml": "taplo format",
|
||||
"lint": "oxlint",
|
||||
"prepublishOnly": "pinst --disable && napi prepublish -t npm",
|
||||
"postpublish": "pinst --enable",
|
||||
"test:ci": "ava -c 1",
|
||||
"test": "ava",
|
||||
"e2e": "yarn workspace @napi-rs/canvas-e2e-webpack test",
|
||||
"version": "napi version && conventional-changelog -p angular -i CHANGELOG.md -s && git add ."
|
||||
},
|
||||
"devDependencies": {
|
||||
"@jimp/core": "^1.6.0",
|
||||
"@jimp/custom": "^0.22.12",
|
||||
"@jimp/jpeg": "^0.22.12",
|
||||
"@jimp/png": "^0.22.12",
|
||||
"@napi-rs/cli": "^3.5.0",
|
||||
"@napi-rs/webcodecs": "^1.1.1",
|
||||
"@octokit/rest": "^22.0.1",
|
||||
"@oxc-node/cli": "^0.1.0",
|
||||
"@oxc-node/core": "^0.1.0",
|
||||
"@taplo/cli": "^0.7.0",
|
||||
"@types/lodash": "^4.17.21",
|
||||
"@types/node": "^25.0.3",
|
||||
"@types/semver": "^7",
|
||||
"ava": "^7.0.0",
|
||||
"canvas": "^3.2.0",
|
||||
"canvaskit-wasm": "^0.41.0",
|
||||
"colorette": "^2.0.20",
|
||||
"conventional-changelog-cli": "^5.0.0",
|
||||
"core-js": "^3.47.0",
|
||||
"echarts": "^6.0.0",
|
||||
"electron": "^41.0.0",
|
||||
"husky": "^9.1.7",
|
||||
"lint-staged": "^16.2.7",
|
||||
"lodash": "^4.17.21",
|
||||
"npm-run-all2": "^8.0.4",
|
||||
"oxlint": "^1.34.0",
|
||||
"oxlint-tsgolint": "^0.22.0",
|
||||
"pinst": "^3.0.0",
|
||||
"png.js": "^0.2.1",
|
||||
"prettier": "^3.7.4",
|
||||
"pretty-bytes": "^7.1.0",
|
||||
"semver": "^7.7.3",
|
||||
"skia-canvas": "^3.0.8",
|
||||
"table": "^6.9.0",
|
||||
"tinybench": "^6.0.0",
|
||||
"typescript": "^6.0.0"
|
||||
},
|
||||
"lint-staged": {
|
||||
"*.@(js|ts|tsx|yml|yaml|md|json|html)": [
|
||||
"prettier --write"
|
||||
],
|
||||
"*.@(js|ts|tsx)": [
|
||||
"oxlint --fix"
|
||||
]
|
||||
},
|
||||
"ava": {
|
||||
"nodeArguments": [
|
||||
"--import=@oxc-node/core/register",
|
||||
"--import=core-js/proposals/promise-with-resolvers.js"
|
||||
],
|
||||
"extensions": {
|
||||
"ts": "module"
|
||||
},
|
||||
"files": [
|
||||
"__test__/**/*.spec.ts",
|
||||
"scripts/__test__/**/*.spec.ts"
|
||||
],
|
||||
"workerThreads": false,
|
||||
"cache": false,
|
||||
"timeout": "3m",
|
||||
"environmentVariables": {
|
||||
"SWC_NODE_PROJECT": "./tsconfig.json",
|
||||
"NODE_ENV": "ava"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 120,
|
||||
"semi": false,
|
||||
"trailingComma": "all",
|
||||
"singleQuote": true,
|
||||
"arrowParens": "always"
|
||||
},
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/Brooooooklyn"
|
||||
},
|
||||
"packageManager": "yarn@4.14.1",
|
||||
"dependenciesMeta": {
|
||||
"canvas": {
|
||||
"built": true
|
||||
},
|
||||
"skia-canvas": {
|
||||
"built": true
|
||||
}
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@napi-rs/canvas-linux-x64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-x64": "0.1.100",
|
||||
"@napi-rs/canvas-win32-x64-msvc": "0.1.100",
|
||||
"@napi-rs/canvas-win32-arm64-msvc": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100",
|
||||
"@napi-rs/canvas-linux-x64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-gnu": "0.1.100",
|
||||
"@napi-rs/canvas-linux-arm64-musl": "0.1.100",
|
||||
"@napi-rs/canvas-darwin-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-android-arm64": "0.1.100",
|
||||
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.100"
|
||||
}
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
PROPRIETARY AND CONFIDENTIAL
|
||||
|
||||
Copyright © 2026 OpenAI, L.L.C.
|
||||
All rights reserved.
|
||||
|
||||
This software and any associated materials (the "Software") are proprietary and confidential information of OpenAI, L.L.C.
|
||||
|
||||
Subject to your compliance with this license, OpenAI, L.L.C. grants you a non-exclusive, non-transferable, revocable, limited license to use the Software solely for internal evaluation and testing purposes.
|
||||
|
||||
You may not, without prior written permission from OpenAI, L.L.C.:
|
||||
- Copy, modify, distribute, sublicense, sell, lease, or otherwise make the Software available to any third party
|
||||
- Use the Software for production, commercial, or benchmarking purposes
|
||||
- Reverse engineer, decompile, disassemble, or otherwise attempt to derive the source code, underlying ideas, algorithms, or models of the Software
|
||||
- Remove or obscure any proprietary notices
|
||||
|
||||
The Software is provided "AS IS", without warranty of any kind, express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement.
|
||||
|
||||
This license is effective until terminated. OpenAI, L.L.C. may terminate this license at any time, with or without notice. Upon termination, you must immediately cease all use of the Software and destroy all copies in your possession or control.
|
||||
Generated
Vendored
+3213
File diff suppressed because one or more lines are too long
Generated
Vendored
+2
File diff suppressed because one or more lines are too long
tmp/ambient-matrix-inspect/node_modules/@oai/artifact-tool/dist/presentation-jsx/jsx-dev-runtime.mjs
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
|
||||
var s=Symbol.for("@oai/granola/presentation-jsx.element");var r=Symbol.for("@oai/granola/presentation-jsx.fragment");function o(e,t,...a){return n(e,t)}function n(e,t){if(typeof e==="function"){return e(t)}return{$$type:s,type:e,props:t}}export{r as Fragment,o as jsxDEV};
|
||||
Generated
Vendored
+2
@@ -0,0 +1,2 @@
|
||||
import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
|
||||
var s=Symbol.for("@oai/granola/presentation-jsx.element");var r=Symbol.for("@oai/granola/presentation-jsx.fragment");function o(e,t){return n(e,t)}function a(e,t){return n(e,t)}function n(e,t){if(typeof e==="function"){return e(t)}return{$$type:s,type:e,props:t}}export{r as Fragment,o as jsx,a as jsxs};
|
||||
Generated
Vendored
+1
@@ -0,0 +1 @@
|
||||
{"packages":{".":{"url":"..","dependencies":{"@bufbuild/protobuf":"@bufbuild/protobuf","@oai/artifact-tool":".","@oai/embedded-fonts":"@oai/embedded-fonts","@oai/walnut":"@oai/walnut","agent-base":"agent-base","debug":"debug","detect-libc":"detect-libc","follow-redirects":"follow-redirects","https-proxy-agent":"https-proxy-agent","ms":"ms","parenthesis":"parenthesis","skia-canvas":"skia-canvas","string-split-by":"string-split-by"}},"@bufbuild/protobuf":{"url":"./@bufbuild/protobuf","dependencies":{"@bufbuild/protobuf":"@bufbuild/protobuf"}},"@oai/embedded-fonts":{"url":"./@oai/embedded-fonts","dependencies":{"@oai/embedded-fonts":"@oai/embedded-fonts"}},"@oai/walnut":{"url":"./@oai/walnut","dependencies":{"@bufbuild/protobuf":"@bufbuild/protobuf","@oai/walnut":"@oai/walnut"}},"agent-base":{"url":"./agent-base","dependencies":{"agent-base":"agent-base"}},"debug":{"url":"./debug","dependencies":{"debug":"debug","ms":"ms"}},"detect-libc":{"url":"./detect-libc","dependencies":{"detect-libc":"detect-libc"}},"follow-redirects":{"url":"./follow-redirects","dependencies":{"follow-redirects":"follow-redirects"}},"https-proxy-agent":{"url":"./https-proxy-agent","dependencies":{"agent-base":"agent-base","debug":"debug","https-proxy-agent":"https-proxy-agent"}},"ms":{"url":"./ms","dependencies":{"ms":"ms"}},"parenthesis":{"url":"./parenthesis","dependencies":{"parenthesis":"parenthesis"}},"skia-canvas":{"url":"./skia-canvas","dependencies":{"detect-libc":"detect-libc","follow-redirects":"follow-redirects","https-proxy-agent":"https-proxy-agent","skia-canvas":"skia-canvas","string-split-by":"string-split-by"}},"string-split-by":{"url":"./string-split-by","dependencies":{"parenthesis":"parenthesis","string-split-by":"string-split-by"}}}}
|
||||
Generated
Vendored
+42
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"lastValidatedTimestamp": 1785188858362,
|
||||
"projects": {
|
||||
"C:\\Users\\runneradmin\\AppData\\Local\\Temp\\tmp.DfskCJdOJi": {
|
||||
"name": "@oai/artifact-tool",
|
||||
"version": "2.8.33"
|
||||
}
|
||||
},
|
||||
"pnpmfiles": [],
|
||||
"settings": {
|
||||
"enableGlobalVirtualStore": false,
|
||||
"allowBuilds": {
|
||||
"skia-canvas": true
|
||||
},
|
||||
"autoInstallPeers": true,
|
||||
"catalogs": {},
|
||||
"dedupeDirectDeps": false,
|
||||
"dedupeInjectedDeps": true,
|
||||
"dedupePeerDependents": true,
|
||||
"dedupePeers": false,
|
||||
"dev": false,
|
||||
"excludeLinksFromLockfile": false,
|
||||
"hoistPattern": [
|
||||
"*"
|
||||
],
|
||||
"hoistWorkspacePackages": true,
|
||||
"injectWorkspacePackages": false,
|
||||
"linkWorkspacePackages": false,
|
||||
"minimumReleaseAge": 1440,
|
||||
"minimumReleaseAgeIgnoreMissingTime": true,
|
||||
"nodeLinker": "hoisted",
|
||||
"optional": true,
|
||||
"peersSuffixMaxLength": 1000,
|
||||
"preferWorkspacePackages": false,
|
||||
"production": true,
|
||||
"publicHoistPattern": [],
|
||||
"workspacePackagePatterns": [
|
||||
"."
|
||||
]
|
||||
},
|
||||
"filteredInstall": false
|
||||
}
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
# `@oai/embedded-fonts`
|
||||
|
||||
`@oai/embedded-fonts` owns the Rust decoder, its WebAssembly build, the
|
||||
TypeScript ABI wrapper, and decoder policy/types for fonts embedded in Office
|
||||
documents.
|
||||
|
||||
The published package contains a version-matched
|
||||
`embedded_font_decoder.wasm`. Runtime consumers import the package normally and
|
||||
call `loadEmbeddedFontDecoder()` only when an imported document contains
|
||||
embedded fonts. The consumer owns lifecycle, caching, and font registration.
|
||||
|
||||
The generated WASM is intentionally not committed. CI builds it from
|
||||
`rust/src`, stages it at the package root, verifies the packed tarball, and then
|
||||
publishes that tarball.
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
import type { EmbeddedFontDecoder } from "./types.js";
|
||||
export * from "./types.js";
|
||||
export { createEmbeddedFontWasmDecoder, decodeEmbeddedFontWithWasm, type EmbeddedFontDecoderWasmSource, } from "./wasm-decoder.js";
|
||||
export declare const EMBEDDED_FONT_DECODER_WASM_EXPORT = "@oai/embedded-fonts/embedded_font_decoder.wasm";
|
||||
/**
|
||||
* Resolves the WASM shipped by the installed package version and creates a
|
||||
* decoder. Runtime hosts own lazy/process-wide caching of the returned decoder.
|
||||
*/
|
||||
export declare function loadEmbeddedFontDecoder(): Promise<EmbeddedFontDecoder>;
|
||||
Generated
Vendored
+17
@@ -0,0 +1,17 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { createRequire } from "node:module";
|
||||
import { createEmbeddedFontWasmDecoder, } from "./wasm-decoder.js";
|
||||
export * from "./types.js";
|
||||
export { createEmbeddedFontWasmDecoder, decodeEmbeddedFontWithWasm, } from "./wasm-decoder.js";
|
||||
export const EMBEDDED_FONT_DECODER_WASM_EXPORT = "@oai/embedded-fonts/embedded_font_decoder.wasm";
|
||||
/**
|
||||
* Resolves the WASM shipped by the installed package version and creates a
|
||||
* decoder. Runtime hosts own lazy/process-wide caching of the returned decoder.
|
||||
*/
|
||||
export async function loadEmbeddedFontDecoder() {
|
||||
const require = createRequire(import.meta.url);
|
||||
const wasmPath = require.resolve(EMBEDDED_FONT_DECODER_WASM_EXPORT);
|
||||
const bytes = await readFile(wasmPath);
|
||||
const source = bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
||||
return createEmbeddedFontWasmDecoder(source);
|
||||
}
|
||||
Generated
Vendored
+81
@@ -0,0 +1,81 @@
|
||||
export type EmbeddedFontDecodePolicy = {
|
||||
allowRestrictedRendering: boolean;
|
||||
maxInputBytes: number;
|
||||
maxOutputBytes: number;
|
||||
};
|
||||
export declare const DEFAULT_EMBEDDED_FONT_DECODE_POLICY: EmbeddedFontDecodePolicy;
|
||||
export type EmbeddedFontFaceType = "regular" | "bold" | "italic" | "boldItalic" | "unknown";
|
||||
export declare function embeddedFontFaceType(type: number | undefined): EmbeddedFontFaceType;
|
||||
export type EmbeddedFontSource = {
|
||||
sourceFamily: string;
|
||||
sourceFaceType: EmbeddedFontFaceType;
|
||||
contentType: string;
|
||||
relationshipId?: string;
|
||||
data: Uint8Array;
|
||||
};
|
||||
export type EmbeddedFontDecodeStatus = "decoded" | "unsupported_format" | "restricted" | "subsetted" | "decode_failed";
|
||||
export type EmbeddedFontDiagnostic = {
|
||||
code: string;
|
||||
severity: "info" | "warning" | "error";
|
||||
message: string;
|
||||
relationshipId?: string;
|
||||
family?: string;
|
||||
};
|
||||
export type EmbeddedFontSfntVersion = "TrueType" | "OpenTypeCff" | "AppleTrueType" | "PostScriptType1";
|
||||
export type EmbeddedFontDecodeResult = {
|
||||
status: EmbeddedFontDecodeStatus;
|
||||
sourceFamily: string;
|
||||
sourceFaceType: EmbeddedFontFaceType;
|
||||
contentType: string;
|
||||
relationshipId?: string;
|
||||
inputSha256: string;
|
||||
outputSha256?: string;
|
||||
decodedFont?: Uint8Array;
|
||||
outputFormat?: "ttf" | "otf" | "ttc" | "woff" | "woff2";
|
||||
metadata: {
|
||||
eot?: {
|
||||
version?: 1 | 2 | 3;
|
||||
flags?: number;
|
||||
subsetted?: boolean;
|
||||
compressed?: boolean;
|
||||
xorEncrypted?: boolean;
|
||||
permissions?: number;
|
||||
familyName?: string;
|
||||
styleName?: string;
|
||||
fullName?: string;
|
||||
rootStrings?: string[];
|
||||
};
|
||||
sfnt?: {
|
||||
version?: EmbeddedFontSfntVersion;
|
||||
tableTags?: string[];
|
||||
};
|
||||
};
|
||||
registration?: {
|
||||
family?: string;
|
||||
subfamily?: string;
|
||||
fullName?: string;
|
||||
weight?: number;
|
||||
italic?: boolean;
|
||||
cmap: {
|
||||
subtableCount: number;
|
||||
selectedPlatformId: number;
|
||||
selectedEncodingId: number;
|
||||
selectedFormat: number;
|
||||
codepointCount: number;
|
||||
firstCodepoint?: number;
|
||||
lastCodepoint?: number;
|
||||
hasBasicLatin: boolean;
|
||||
};
|
||||
};
|
||||
diagnostics: EmbeddedFontDiagnostic[];
|
||||
};
|
||||
export type EmbeddedFontDecodeInput = {
|
||||
sourceFamily: string;
|
||||
sourceFaceType: EmbeddedFontFaceType;
|
||||
contentType: string;
|
||||
relationshipId?: string;
|
||||
inputSha256: string;
|
||||
data: Uint8Array;
|
||||
policy?: Partial<EmbeddedFontDecodePolicy>;
|
||||
};
|
||||
export type EmbeddedFontDecoder = (input: EmbeddedFontDecodeInput) => EmbeddedFontDecodeResult | Promise<EmbeddedFontDecodeResult>;
|
||||
Generated
Vendored
+19
@@ -0,0 +1,19 @@
|
||||
export const DEFAULT_EMBEDDED_FONT_DECODE_POLICY = {
|
||||
allowRestrictedRendering: false,
|
||||
maxInputBytes: 64 * 1024 * 1024,
|
||||
maxOutputBytes: 64 * 1024 * 1024,
|
||||
};
|
||||
export function embeddedFontFaceType(type) {
|
||||
switch (type) {
|
||||
case 1:
|
||||
return "regular";
|
||||
case 2:
|
||||
return "bold";
|
||||
case 3:
|
||||
return "italic";
|
||||
case 4:
|
||||
return "boldItalic";
|
||||
default:
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import type { EmbeddedFontDecodeInput, EmbeddedFontDecodeResult, EmbeddedFontDecoder } from "./types.js";
|
||||
type EmbeddedFontDecoderWasmExports = {
|
||||
memory: WebAssembly.Memory;
|
||||
embedded_font_decoder_alloc: (size: number) => number;
|
||||
embedded_font_decoder_free: (ptr: number, capacity: number) => void;
|
||||
embedded_font_decoder_decode_with_limits: (ptr: number, len: number, maxInputBytes: number, maxOutputBytes: number) => number;
|
||||
embedded_font_decoder_result_ptr: () => number;
|
||||
embedded_font_decoder_result_len: () => number;
|
||||
embedded_font_decoder_clear_result: () => void;
|
||||
};
|
||||
export type EmbeddedFontDecoderWasmSource = BufferSource | WebAssembly.Module;
|
||||
export declare function createEmbeddedFontWasmDecoder(source: EmbeddedFontDecoderWasmSource): Promise<EmbeddedFontDecoder>;
|
||||
export declare function decodeEmbeddedFontWithWasm(input: EmbeddedFontDecodeInput, wasm: EmbeddedFontDecoderWasmExports): Promise<EmbeddedFontDecodeResult>;
|
||||
export {};
|
||||
Generated
Vendored
+441
@@ -0,0 +1,441 @@
|
||||
import { DEFAULT_EMBEDDED_FONT_DECODE_POLICY } from "./types.js";
|
||||
const SUPPORTED_EOT_CONTENT_TYPES = new Set([
|
||||
"application/x-fontdata",
|
||||
"application/vnd.ms-fontobject",
|
||||
]);
|
||||
const textDecoder = new TextDecoder();
|
||||
export async function createEmbeddedFontWasmDecoder(source) {
|
||||
const wasm = await instantiateEmbeddedFontDecoderWasm(source);
|
||||
return (input) => decodeEmbeddedFontWithWasm(input, wasm);
|
||||
}
|
||||
export async function decodeEmbeddedFontWithWasm(input, wasm) {
|
||||
const policy = resolvePolicy(input.policy);
|
||||
if (!SUPPORTED_EOT_CONTENT_TYPES.has(input.contentType)) {
|
||||
return statusResult(input, "unsupported_format", [
|
||||
{
|
||||
code: "embedded_font_unsupported_content_type",
|
||||
severity: "warning",
|
||||
message: `Unsupported embedded font content type "${input.contentType}".`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
if (input.data.byteLength > policy.maxInputBytes) {
|
||||
return statusResult(input, "decode_failed", [
|
||||
{
|
||||
code: "embedded_font_input_too_large",
|
||||
severity: "error",
|
||||
message: `Embedded font input has ${input.data.byteLength} bytes, exceeding limit ${policy.maxInputBytes}.`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
let frame;
|
||||
try {
|
||||
frame = decodeWasmFrame(wasm, input.data, policy);
|
||||
}
|
||||
catch (error) {
|
||||
return statusResult(input, "decode_failed", [
|
||||
{
|
||||
code: "embedded_font_wasm_decode_failed",
|
||||
severity: "error",
|
||||
message: error instanceof Error
|
||||
? error.message
|
||||
: `Embedded font WASM decode failed: ${String(error)}`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
const { header, decodedFont } = parseWasmFrame(frame);
|
||||
if (!header.ok) {
|
||||
return statusResult(input, header.status ?? "decode_failed", [
|
||||
{
|
||||
code: "embedded_font_decode_failed",
|
||||
severity: "error",
|
||||
message: header.message,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
]);
|
||||
}
|
||||
const diagnostics = header.warnings.map((warning) => warningDiagnostic(warning, input));
|
||||
if (!header.policy.renderRegistrationAllowed &&
|
||||
!policy.allowRestrictedRendering) {
|
||||
return statusResult(input, "restricted", [
|
||||
...diagnostics,
|
||||
{
|
||||
code: "embedded_font_restricted_by_policy",
|
||||
severity: "warning",
|
||||
message: `Embedded font permission "${header.policy.permission}" is not allowed for render registration.`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
], header);
|
||||
}
|
||||
if (header.metadata.eot?.subsetted === true) {
|
||||
return statusResult(input, "subsetted", [
|
||||
...diagnostics,
|
||||
{
|
||||
code: "embedded_font_subset_missing_glyph",
|
||||
severity: "warning",
|
||||
message: "Embedded font is subsetted; glyph coverage checks are not available yet.",
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
], header);
|
||||
}
|
||||
if (decodedFont.byteLength > policy.maxOutputBytes) {
|
||||
return statusResult(input, "decode_failed", [
|
||||
...diagnostics,
|
||||
{
|
||||
code: "embedded_font_output_too_large",
|
||||
severity: "error",
|
||||
message: `Decoded embedded font has ${decodedFont.byteLength} bytes, exceeding limit ${policy.maxOutputBytes}.`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
},
|
||||
], header);
|
||||
}
|
||||
return {
|
||||
status: "decoded",
|
||||
sourceFamily: input.sourceFamily,
|
||||
sourceFaceType: input.sourceFaceType,
|
||||
contentType: input.contentType,
|
||||
relationshipId: input.relationshipId,
|
||||
inputSha256: input.inputSha256,
|
||||
outputSha256: await sha256WithWebCrypto(decodedFont),
|
||||
decodedFont,
|
||||
outputFormat: header.outputFormat,
|
||||
metadata: header.metadata,
|
||||
registration: header.registration,
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
async function instantiateEmbeddedFontDecoderWasm(source) {
|
||||
const instance = await instantiateWasm(source);
|
||||
return readWasmExports(instance);
|
||||
}
|
||||
async function instantiateWasm(source) {
|
||||
if (source instanceof WebAssembly.Module) {
|
||||
return WebAssembly.instantiate(source, {});
|
||||
}
|
||||
const module = await WebAssembly.compile(source);
|
||||
return WebAssembly.instantiate(module, {});
|
||||
}
|
||||
function readWasmExports(instance) {
|
||||
const exports = instance.exports;
|
||||
const memory = exports["memory"];
|
||||
if (!(memory instanceof WebAssembly.Memory)) {
|
||||
throw new Error("Embedded font decoder WASM did not export memory.");
|
||||
}
|
||||
const alloc = requiredWasmFunction(exports, "embedded_font_decoder_alloc");
|
||||
const free = requiredWasmFunction(exports, "embedded_font_decoder_free");
|
||||
const decodeWithLimits = requiredWasmFunction(exports, "embedded_font_decoder_decode_with_limits");
|
||||
const resultPtr = requiredWasmFunction(exports, "embedded_font_decoder_result_ptr");
|
||||
const resultLen = requiredWasmFunction(exports, "embedded_font_decoder_result_len");
|
||||
const clearResult = requiredWasmFunction(exports, "embedded_font_decoder_clear_result");
|
||||
return {
|
||||
memory,
|
||||
embedded_font_decoder_alloc: alloc,
|
||||
embedded_font_decoder_free: free,
|
||||
embedded_font_decoder_decode_with_limits: decodeWithLimits,
|
||||
embedded_font_decoder_result_ptr: resultPtr,
|
||||
embedded_font_decoder_result_len: resultLen,
|
||||
embedded_font_decoder_clear_result: clearResult,
|
||||
};
|
||||
}
|
||||
function requiredWasmFunction(exports, name) {
|
||||
const value = exports[name];
|
||||
if (typeof value !== "function") {
|
||||
throw new Error(`Embedded font decoder WASM missing export ${name}.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function decodeWasmFrame(wasm, data, policy) {
|
||||
const inputPtr = wasm.embedded_font_decoder_alloc(data.byteLength);
|
||||
try {
|
||||
new Uint8Array(wasm.memory.buffer, inputPtr, data.byteLength).set(data);
|
||||
wasm.embedded_font_decoder_decode_with_limits(inputPtr, data.byteLength, policy.maxInputBytes, policy.maxOutputBytes);
|
||||
const resultPtr = wasm.embedded_font_decoder_result_ptr();
|
||||
const resultLen = wasm.embedded_font_decoder_result_len();
|
||||
if (resultPtr === 0 || resultLen === 0) {
|
||||
throw new Error("Embedded font decoder WASM returned an empty result.");
|
||||
}
|
||||
return new Uint8Array(new Uint8Array(wasm.memory.buffer, resultPtr, resultLen));
|
||||
}
|
||||
finally {
|
||||
wasm.embedded_font_decoder_free(inputPtr, data.byteLength);
|
||||
wasm.embedded_font_decoder_clear_result();
|
||||
}
|
||||
}
|
||||
function parseWasmFrame(frame) {
|
||||
if (frame.byteLength < 4) {
|
||||
throw new Error("Embedded font decoder WASM result frame is too small.");
|
||||
}
|
||||
const jsonLength = new DataView(frame.buffer, frame.byteOffset, 4).getUint32(0, true);
|
||||
const decodedOffset = 4 + jsonLength;
|
||||
if (decodedOffset > frame.byteLength) {
|
||||
throw new Error("Embedded font decoder WASM result frame is truncated.");
|
||||
}
|
||||
const headerText = textDecoder.decode(frame.subarray(4, decodedOffset));
|
||||
const parsed = JSON.parse(headerText);
|
||||
return {
|
||||
header: parseWasmDecodeHeader(parsed),
|
||||
decodedFont: new Uint8Array(frame.subarray(decodedOffset)),
|
||||
};
|
||||
}
|
||||
function parseWasmDecodeHeader(value) {
|
||||
const header = objectValue(value, "WASM decode header");
|
||||
const ok = header["ok"];
|
||||
if (ok === true) {
|
||||
return {
|
||||
ok: true,
|
||||
outputFormat: outputFormatValue(header["outputFormat"]),
|
||||
metadata: metadataValue(header["metadata"]),
|
||||
registration: registrationValue(header["registration"]),
|
||||
policy: policyValue(header["policy"]),
|
||||
warnings: warningsValue(header["warnings"]),
|
||||
};
|
||||
}
|
||||
if (ok === false) {
|
||||
return {
|
||||
ok: false,
|
||||
status: typeof header["status"] === "string"
|
||||
? decodeStatusValue(header["status"])
|
||||
: undefined,
|
||||
message: stringValue(header["message"], "WASM decode error message"),
|
||||
};
|
||||
}
|
||||
throw new Error("Embedded font decoder WASM header missing ok boolean.");
|
||||
}
|
||||
function metadataValue(value) {
|
||||
const metadata = objectValue(value, "WASM metadata");
|
||||
return {
|
||||
eot: metadata["eot"] === undefined
|
||||
? undefined
|
||||
: eotMetadataValue(metadata["eot"]),
|
||||
sfnt: metadata["sfnt"] === undefined
|
||||
? undefined
|
||||
: sfntMetadataValue(metadata["sfnt"]),
|
||||
};
|
||||
}
|
||||
function eotMetadataValue(value) {
|
||||
const eot = objectValue(value, "WASM EOT metadata");
|
||||
return {
|
||||
version: eot["version"] === undefined
|
||||
? undefined
|
||||
: eotVersionValue(eot["version"]),
|
||||
flags: optionalNumberValue(eot["flags"], "EOT flags"),
|
||||
subsetted: optionalBooleanValue(eot["subsetted"], "EOT subsetted"),
|
||||
compressed: optionalBooleanValue(eot["compressed"], "EOT compressed"),
|
||||
xorEncrypted: optionalBooleanValue(eot["xorEncrypted"], "EOT xorEncrypted"),
|
||||
permissions: optionalNumberValue(eot["permissions"], "EOT permissions"),
|
||||
familyName: optionalStringValue(eot["familyName"], "EOT familyName"),
|
||||
styleName: optionalStringValue(eot["styleName"], "EOT styleName"),
|
||||
fullName: optionalStringValue(eot["fullName"], "EOT fullName"),
|
||||
};
|
||||
}
|
||||
function sfntMetadataValue(value) {
|
||||
const sfnt = objectValue(value, "WASM SFNT metadata");
|
||||
return {
|
||||
version: sfnt["version"] === undefined
|
||||
? undefined
|
||||
: sfntVersionValue(sfnt["version"]),
|
||||
tableTags: sfnt["tableTags"] === undefined
|
||||
? undefined
|
||||
: stringArrayValue(sfnt["tableTags"], "SFNT tableTags"),
|
||||
};
|
||||
}
|
||||
function registrationValue(value) {
|
||||
const registration = objectValue(value, "WASM registration metadata");
|
||||
const cmap = objectValue(registration["cmap"], "WASM cmap metadata");
|
||||
return {
|
||||
family: optionalStringValue(registration["family"], "registration family"),
|
||||
subfamily: optionalStringValue(registration["subfamily"], "registration subfamily"),
|
||||
fullName: optionalStringValue(registration["fullName"], "registration fullName"),
|
||||
weight: optionalNumberValue(registration["weight"], "registration weight"),
|
||||
italic: optionalBooleanValue(registration["italic"], "registration italic"),
|
||||
cmap: {
|
||||
subtableCount: numberValue(cmap["subtableCount"], "cmap subtableCount"),
|
||||
selectedPlatformId: numberValue(cmap["selectedPlatformId"], "cmap selectedPlatformId"),
|
||||
selectedEncodingId: numberValue(cmap["selectedEncodingId"], "cmap selectedEncodingId"),
|
||||
selectedFormat: numberValue(cmap["selectedFormat"], "cmap selectedFormat"),
|
||||
codepointCount: numberValue(cmap["codepointCount"], "cmap codepointCount"),
|
||||
firstCodepoint: optionalNumberValue(cmap["firstCodepoint"], "cmap firstCodepoint"),
|
||||
lastCodepoint: optionalNumberValue(cmap["lastCodepoint"], "cmap lastCodepoint"),
|
||||
hasBasicLatin: booleanValue(cmap["hasBasicLatin"], "cmap hasBasicLatin"),
|
||||
},
|
||||
};
|
||||
}
|
||||
function policyValue(value) {
|
||||
const policy = objectValue(value, "WASM embedded font policy");
|
||||
return {
|
||||
permission: stringValue(policy["permission"], "policy permission"),
|
||||
renderRegistrationAllowed: booleanValue(policy["renderRegistrationAllowed"], "policy renderRegistrationAllowed"),
|
||||
editableReuseAllowed: booleanValue(policy["editableReuseAllowed"], "policy editableReuseAllowed"),
|
||||
reexportAllowed: booleanValue(policy["reexportAllowed"], "policy reexportAllowed"),
|
||||
rawBytesRoundTripAllowed: booleanValue(policy["rawBytesRoundTripAllowed"], "policy rawBytesRoundTripAllowed"),
|
||||
noSubsetting: booleanValue(policy["noSubsetting"], "policy noSubsetting"),
|
||||
bitmapEmbeddingOnly: booleanValue(policy["bitmapEmbeddingOnly"], "policy bitmapEmbeddingOnly"),
|
||||
};
|
||||
}
|
||||
function warningsValue(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error("WASM warnings must be an array.");
|
||||
}
|
||||
return value.map((entry) => {
|
||||
const warning = objectValue(entry, "WASM warning");
|
||||
const kind = stringValue(warning["kind"], "warning kind");
|
||||
if (kind === "name_mismatch") {
|
||||
return {
|
||||
kind,
|
||||
field: stringValue(warning["field"], "warning field"),
|
||||
eot: stringValue(warning["eot"], "warning eot"),
|
||||
sfnt: stringValue(warning["sfnt"], "warning sfnt"),
|
||||
};
|
||||
}
|
||||
if (kind === "missing_decoded_name") {
|
||||
return {
|
||||
kind,
|
||||
field: stringValue(warning["field"], "warning field"),
|
||||
eot: stringValue(warning["eot"], "warning eot"),
|
||||
};
|
||||
}
|
||||
throw new Error(`Unknown WASM warning kind ${kind}.`);
|
||||
});
|
||||
}
|
||||
function statusResult(input, status, diagnostics, header) {
|
||||
return {
|
||||
status,
|
||||
sourceFamily: input.sourceFamily,
|
||||
sourceFaceType: input.sourceFaceType,
|
||||
contentType: input.contentType,
|
||||
relationshipId: input.relationshipId,
|
||||
inputSha256: input.inputSha256,
|
||||
metadata: header?.metadata ?? {},
|
||||
registration: header?.registration,
|
||||
diagnostics,
|
||||
};
|
||||
}
|
||||
function warningDiagnostic(warning, input) {
|
||||
if (warning.kind === "name_mismatch") {
|
||||
return {
|
||||
code: "embedded_font_name_mismatch",
|
||||
severity: "warning",
|
||||
message: `EOT ${warning.field} "${warning.eot}" does not match decoded SFNT name "${warning.sfnt}".`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
};
|
||||
}
|
||||
return {
|
||||
code: "embedded_font_missing_decoded_name",
|
||||
severity: "warning",
|
||||
message: `EOT ${warning.field} "${warning.eot}" is missing from decoded SFNT name metadata.`,
|
||||
family: input.sourceFamily,
|
||||
relationshipId: input.relationshipId,
|
||||
};
|
||||
}
|
||||
function resolvePolicy(policy) {
|
||||
return {
|
||||
...DEFAULT_EMBEDDED_FONT_DECODE_POLICY,
|
||||
...policy,
|
||||
};
|
||||
}
|
||||
function objectValue(value, field) {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error(`${field} must be an object.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function stringValue(value, field) {
|
||||
if (typeof value !== "string") {
|
||||
throw new Error(`${field} must be a string.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function optionalStringValue(value, field) {
|
||||
return value === undefined ? undefined : stringValue(value, field);
|
||||
}
|
||||
function numberValue(value, field) {
|
||||
if (typeof value !== "number" || !Number.isFinite(value)) {
|
||||
throw new Error(`${field} must be a finite number.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function optionalNumberValue(value, field) {
|
||||
return value === undefined ? undefined : numberValue(value, field);
|
||||
}
|
||||
function booleanValue(value, field) {
|
||||
if (typeof value !== "boolean") {
|
||||
throw new Error(`${field} must be a boolean.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
function optionalBooleanValue(value, field) {
|
||||
return value === undefined ? undefined : booleanValue(value, field);
|
||||
}
|
||||
function stringArrayValue(value, field) {
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`${field} must be an array.`);
|
||||
}
|
||||
return value.map((entry) => stringValue(entry, field));
|
||||
}
|
||||
function outputFormatValue(value) {
|
||||
const format = stringValue(value, "output format");
|
||||
switch (format) {
|
||||
case "otf":
|
||||
case "ttc":
|
||||
case "ttf":
|
||||
case "woff":
|
||||
case "woff2":
|
||||
return format;
|
||||
}
|
||||
throw new Error(`Unsupported output format ${format}.`);
|
||||
}
|
||||
function decodeStatusValue(value) {
|
||||
switch (value) {
|
||||
case "decoded":
|
||||
case "unsupported_format":
|
||||
case "restricted":
|
||||
case "subsetted":
|
||||
case "decode_failed":
|
||||
return value;
|
||||
}
|
||||
throw new Error(`Unsupported decode status ${value}.`);
|
||||
}
|
||||
function eotVersionValue(value) {
|
||||
switch (numberValue(value, "EOT version")) {
|
||||
case 1:
|
||||
return 1;
|
||||
case 2:
|
||||
return 2;
|
||||
case 3:
|
||||
return 3;
|
||||
default:
|
||||
throw new Error(`Unsupported EOT version ${String(value)}.`);
|
||||
}
|
||||
}
|
||||
function sfntVersionValue(value) {
|
||||
const version = stringValue(value, "SFNT version");
|
||||
switch (version) {
|
||||
case "AppleTrueType":
|
||||
case "OpenTypeCff":
|
||||
case "PostScriptType1":
|
||||
case "TrueType":
|
||||
return version;
|
||||
}
|
||||
throw new Error(`Unsupported SFNT version ${version}.`);
|
||||
}
|
||||
async function sha256WithWebCrypto(data) {
|
||||
const subtle = globalThis.crypto?.subtle;
|
||||
if (!subtle) {
|
||||
return undefined;
|
||||
}
|
||||
const digest = await subtle.digest("SHA-256", data);
|
||||
return Array.from(new Uint8Array(digest))
|
||||
.map((byte) => byte.toString(16).padStart(2, "0"))
|
||||
.join("");
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
+37
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"name": "@oai/embedded-fonts",
|
||||
"version": "0.1.1",
|
||||
"author": "openai",
|
||||
"private": false,
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"exports": {
|
||||
".": "./dist/index.js",
|
||||
"./embedded_font_decoder.wasm": "./embedded_font_decoder.wasm"
|
||||
},
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"embedded_font_decoder.wasm"
|
||||
],
|
||||
"publishConfig": {
|
||||
"files": [
|
||||
"dist/**/*",
|
||||
"embedded_font_decoder.wasm"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "22.19.17",
|
||||
"bun-types": "1.3.0",
|
||||
"prettier": "3.2.5",
|
||||
"typescript": "5.7.3",
|
||||
"@oai/bun": "0.1.0",
|
||||
"@oai/prettier": "0.1.0"
|
||||
},
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"build": "tsc -p tsconfig.build.json",
|
||||
"format": "prettier --check --cache --ignore-path .gitignore '**/*.{js,jsx,ts,tsx,json,css,scss,md}'",
|
||||
"test": "bun test",
|
||||
"types": "tsc --noEmit"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@oai/walnut",
|
||||
"version": "0.1.231",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
"./wasm": "./wasm",
|
||||
"./wasm/*": "./wasm/*"
|
||||
}
|
||||
}
|
||||
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Generated
Vendored
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user