mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
feat: rendering engine
This commit is contained in:
6
MODULE.bazel.lock
generated
6
MODULE.bazel.lock
generated
@@ -614,6 +614,7 @@
|
||||
"arbitrary_1.4.2": "{\"dependencies\":[{\"name\":\"derive_arbitrary\",\"optional\":true,\"req\":\"~1.4.0\"},{\"kind\":\"dev\",\"name\":\"exhaustigen\",\"req\":\"^0.1.0\"}],\"features\":{\"derive\":[\"derive_arbitrary\"]}}",
|
||||
"arboard_3.6.1": "{\"dependencies\":[{\"features\":[\"std\"],\"name\":\"clipboard-win\",\"req\":\"^5.3.1\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"env_logger\",\"req\":\"^0.10.2\"},{\"default_features\":false,\"features\":[\"png\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"default_features\":false,\"features\":[\"tiff\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"png\",\"bmp\"],\"name\":\"image\",\"optional\":true,\"req\":\"^0.25\",\"target\":\"cfg(windows)\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"log\",\"req\":\"^0.4\",\"target\":\"cfg(windows)\"},{\"name\":\"objc2\",\"req\":\"^0.6.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"objc2-core-graphics\",\"NSPasteboard\",\"NSPasteboardItem\",\"NSImage\"],\"name\":\"objc2-app-kit\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CFCGTypes\"],\"name\":\"objc2-core-foundation\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"CGImage\",\"CGColorSpace\",\"CGDataProvider\"],\"name\":\"objc2-core-graphics\",\"optional\":true,\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"default_features\":false,\"features\":[\"std\",\"NSArray\",\"NSString\",\"NSEnumerator\",\"NSGeometry\",\"NSValue\"],\"name\":\"objc2-foundation\",\"req\":\"^0.3.0\",\"target\":\"cfg(target_os = \\\"macos\\\")\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"percent-encoding\",\"req\":\"^2.3.1\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"features\":[\"Win32_Foundation\",\"Win32_Storage_FileSystem\",\"Win32_System_DataExchange\",\"Win32_System_Memory\",\"Win32_System_Ole\",\"Win32_UI_Shell\"],\"name\":\"windows-sys\",\"req\":\">=0.52.0, <0.61.0\",\"target\":\"cfg(windows)\"},{\"name\":\"wl-clipboard-rs\",\"optional\":true,\"req\":\"^0.9.0\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"},{\"name\":\"x11rb\",\"req\":\"^0.13\",\"target\":\"cfg(all(unix, not(any(target_os=\\\"macos\\\", target_os=\\\"android\\\", target_os=\\\"emscripten\\\"))))\"}],\"features\":{\"core-graphics\":[\"dep:objc2-core-graphics\"],\"default\":[\"image-data\"],\"image\":[\"dep:image\"],\"image-data\":[\"dep:objc2-core-graphics\",\"dep:objc2-core-foundation\",\"image\",\"windows-sys\",\"core-graphics\"],\"wayland-data-control\":[\"wl-clipboard-rs\"],\"windows-sys\":[\"windows-sys/Win32_Graphics_Gdi\"],\"wl-clipboard-rs\":[\"dep:wl-clipboard-rs\"]}}",
|
||||
"arc-swap_1.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"adaptive-barrier\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"~0.7\"},{\"kind\":\"dev\",\"name\":\"crossbeam-utils\",\"req\":\"~0.8\"},{\"kind\":\"dev\",\"name\":\"itertools\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"num_cpus\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"once_cell\",\"req\":\"~1\"},{\"kind\":\"dev\",\"name\":\"parking_lot\",\"req\":\"~0.12\"},{\"kind\":\"dev\",\"name\":\"proptest\",\"req\":\"^1\"},{\"name\":\"rustversion\",\"req\":\"^1\"},{\"features\":[\"rc\"],\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.130\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.177\"}],\"features\":{\"experimental-strategies\":[],\"experimental-thread-local\":[],\"internal-test-strategies\":[],\"weak\":[]}}",
|
||||
"arrayref_0.3.9": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"quickcheck\",\"req\":\"^1.0\"}],\"features\":{}}",
|
||||
"arrayvec_0.7.6": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bencher\",\"req\":\"^0.1.4\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"matches\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1.4\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"ascii-canvas_3.0.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"diff\",\"req\":\"^0.1\"},{\"name\":\"term\",\"req\":\"^0.7\"}],\"features\":{}}",
|
||||
"ascii_1.1.0": "{\"dependencies\":[{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.25\"},{\"name\":\"serde_test\",\"optional\":true,\"req\":\"^1.0\"}],\"features\":{\"alloc\":[],\"default\":[\"std\"],\"std\":[\"alloc\"]}}",
|
||||
@@ -847,6 +848,7 @@
|
||||
"fnv_1.0.7": "{\"dependencies\":[],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"foldhash_0.1.5": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.14\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"std\":[]}}",
|
||||
"foldhash_0.2.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"ahash\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"chrono\",\"req\":\"^0.4\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5\"},{\"kind\":\"dev\",\"name\":\"fxhash\",\"req\":\"^0.2\"},{\"kind\":\"dev\",\"name\":\"hashbrown\",\"req\":\"^0.15\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rapidhash\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"uuid\",\"req\":\"^1.8\"}],\"features\":{\"default\":[\"std\"],\"nightly\":[],\"std\":[]}}",
|
||||
"font8x8_0.3.1": "{\"dependencies\":[],\"features\":{\"default\":[\"unicode\",\"std\"],\"std\":[],\"unicode\":[]}}",
|
||||
"foreign-types-shared_0.1.1": "{\"dependencies\":[],\"features\":{}}",
|
||||
"foreign-types_0.3.2": "{\"dependencies\":[{\"name\":\"foreign-types-shared\",\"req\":\"^0.1\"}],\"features\":{}}",
|
||||
"form_urlencoded_1.2.2": "{\"dependencies\":[{\"default_features\":false,\"name\":\"percent-encoding\",\"req\":\"^2.3.0\"}],\"features\":{\"alloc\":[\"percent-encoding/alloc\"],\"default\":[\"std\"],\"std\":[\"alloc\",\"percent-encoding/std\"]}}",
|
||||
@@ -1130,6 +1132,7 @@
|
||||
"pkcs8_0.10.2": "{\"dependencies\":[{\"features\":[\"oid\"],\"name\":\"der\",\"req\":\"^0.7\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"pkcs5\",\"optional\":true,\"req\":\"^0.7\"},{\"default_features\":false,\"name\":\"rand_core\",\"optional\":true,\"req\":\"^0.6\"},{\"name\":\"spki\",\"req\":\"^0.7.1\"},{\"default_features\":false,\"name\":\"subtle\",\"optional\":true,\"req\":\"^2\"},{\"kind\":\"dev\",\"name\":\"tempfile\",\"req\":\"^3\"}],\"features\":{\"3des\":[\"encryption\",\"pkcs5/3des\"],\"alloc\":[\"der/alloc\",\"der/zeroize\",\"spki/alloc\"],\"des-insecure\":[\"encryption\",\"pkcs5/des-insecure\"],\"encryption\":[\"alloc\",\"pkcs5/alloc\",\"pkcs5/pbes2\",\"rand_core\"],\"getrandom\":[\"rand_core/getrandom\"],\"pem\":[\"alloc\",\"der/pem\",\"spki/pem\"],\"sha1-insecure\":[\"encryption\",\"pkcs5/sha1-insecure\"],\"std\":[\"alloc\",\"der/std\",\"spki/std\"]}}",
|
||||
"pkg-config_0.3.32": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"lazy_static\",\"req\":\"^1\"}],\"features\":{}}",
|
||||
"plist_1.8.0": "{\"dependencies\":[{\"name\":\"base64\",\"req\":\"^0.22.0\"},{\"name\":\"indexmap\",\"req\":\"^2.1.0\"},{\"name\":\"quick_xml\",\"package\":\"quick-xml\",\"req\":\"^0.38.0\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_derive\",\"req\":\"^1.0.2\"},{\"kind\":\"dev\",\"name\":\"serde_yaml\",\"req\":\"^0.8.21\"},{\"features\":[\"parsing\",\"formatting\"],\"name\":\"time\",\"req\":\"^0.3.30\"}],\"features\":{\"default\":[\"serde\"],\"enable_unstable_features_that_may_break_with_minor_version_bumps\":[]}}",
|
||||
"png_0.17.16": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^3.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.4.0\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.11\"},{\"kind\":\"dev\",\"name\":\"getopts\",\"req\":\"^0.2.14\"},{\"default_features\":false,\"features\":[\"glutin\"],\"kind\":\"dev\",\"name\":\"glium\",\"req\":\"^0.32\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.8.4\"},{\"kind\":\"dev\",\"name\":\"term\",\"req\":\"^1.0.1\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"]}}",
|
||||
"png_0.18.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"approx\",\"req\":\"^0.5.1\"},{\"name\":\"bitflags\",\"req\":\"^2.0\"},{\"kind\":\"dev\",\"name\":\"byteorder\",\"req\":\"^1.5.0\"},{\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"clap\",\"req\":\"^4.0\"},{\"name\":\"crc32fast\",\"req\":\"^1.2.0\"},{\"default_features\":false,\"features\":[\"cargo_bench_support\"],\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.7.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"name\":\"fdeflate\",\"req\":\"^0.3.3\"},{\"name\":\"flate2\",\"req\":\"^1.0.35\"},{\"kind\":\"dev\",\"name\":\"glob\",\"req\":\"^0.3\"},{\"features\":[\"simd\"],\"name\":\"miniz_oxide\",\"req\":\"^0.8\"},{\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9.2\"}],\"features\":{\"benchmarks\":[],\"unstable\":[\"crc32fast/nightly\"],\"zlib-rs\":[\"flate2/zlib-rs\"]}}",
|
||||
"polling_3.11.0": "{\"dependencies\":[{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"concurrent-queue\",\"req\":\"^2.2.0\",\"target\":\"cfg(windows)\"},{\"kind\":\"dev\",\"name\":\"easy-parallel\",\"req\":\"^3.1.0\"},{\"kind\":\"dev\",\"name\":\"fastrand\",\"req\":\"^2.0.0\"},{\"name\":\"hermit-abi\",\"req\":\"^0.5.0\",\"target\":\"cfg(target_os = \\\"hermit\\\")\"},{\"kind\":\"dev\",\"name\":\"libc\",\"req\":\"^0.2\",\"target\":\"cfg(unix)\"},{\"name\":\"pin-project-lite\",\"req\":\"^0.2.9\",\"target\":\"cfg(windows)\"},{\"default_features\":false,\"features\":[\"event\",\"fs\",\"pipe\",\"process\",\"std\",\"time\"],\"name\":\"rustix\",\"req\":\"^1.0.5\",\"target\":\"cfg(any(unix, target_os = \\\"fuchsia\\\", target_os = \\\"vxworks\\\"))\"},{\"kind\":\"dev\",\"name\":\"signal-hook\",\"req\":\"^0.3.17\",\"target\":\"cfg(all(unix, not(target_os=\\\"vita\\\")))\"},{\"kind\":\"dev\",\"name\":\"socket2\",\"req\":\"^0.6.0\"},{\"default_features\":false,\"name\":\"tracing\",\"optional\":true,\"req\":\"^0.1.37\"},{\"features\":[\"Wdk_Foundation\",\"Wdk_Storage_FileSystem\",\"Win32_Foundation\",\"Win32_Networking_WinSock\",\"Win32_Security\",\"Win32_Storage_FileSystem\",\"Win32_System_IO\",\"Win32_System_LibraryLoader\",\"Win32_System_Threading\",\"Win32_System_WindowsProgramming\"],\"name\":\"windows-sys\",\"req\":\"^0.61\",\"target\":\"cfg(windows)\"}],\"features\":{}}",
|
||||
"poly1305_0.8.0": "{\"dependencies\":[{\"name\":\"cpufeatures\",\"req\":\"^0.2\",\"target\":\"cfg(any(target_arch = \\\"x86_64\\\", target_arch = \\\"x86\\\"))\"},{\"kind\":\"dev\",\"name\":\"hex-literal\",\"req\":\"^0.3\"},{\"name\":\"opaque-debug\",\"req\":\"^0.3\"},{\"default_features\":false,\"name\":\"universal-hash\",\"req\":\"^0.5\"},{\"default_features\":false,\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"std\":[\"universal-hash/std\"]}}",
|
||||
@@ -1324,6 +1327,7 @@
|
||||
"static_assertions_1.1.0": "{\"dependencies\":[],\"features\":{\"nightly\":[]}}",
|
||||
"stop-words_0.9.0": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"human_regex\",\"req\":\"^0.3.0\"},{\"kind\":\"build\",\"name\":\"serde_json\",\"req\":\"^1\"}],\"features\":{\"constructed\":[],\"default\":[\"iso\"],\"iso\":[],\"nltk\":[],\"unimplemented\":[]}}",
|
||||
"streaming-iterator_0.1.9": "{\"dependencies\":[],\"features\":{\"alloc\":[],\"std\":[\"alloc\"]}}",
|
||||
"strict-num_0.1.1": "{\"dependencies\":[{\"default_features\":false,\"features\":[\"std\"],\"name\":\"float-cmp\",\"optional\":true,\"req\":\"^0.9\"}],\"features\":{\"approx-eq\":[\"float-cmp\"],\"default\":[\"approx-eq\"]}}",
|
||||
"string_cache_0.8.9": "{\"dependencies\":[{\"default_features\":false,\"name\":\"malloc_size_of\",\"optional\":true,\"req\":\"^0.1\"},{\"name\":\"new_debug_unreachable\",\"req\":\"^1.0.2\"},{\"name\":\"parking_lot\",\"req\":\"^0.12\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"precomputed-hash\",\"req\":\"^0.1\"},{\"name\":\"serde\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[\"serde_support\"],\"serde_support\":[\"serde\"]}}",
|
||||
"string_cache_codegen_0.5.4": "{\"dependencies\":[{\"name\":\"phf_generator\",\"req\":\"^0.11\"},{\"name\":\"phf_shared\",\"req\":\"^0.11\"},{\"name\":\"proc-macro2\",\"req\":\"^1\"},{\"name\":\"quote\",\"req\":\"^1\"}],\"features\":{}}",
|
||||
"stringprep_0.1.5": "{\"dependencies\":[{\"name\":\"unicode-bidi\",\"req\":\"^0.3\"},{\"name\":\"unicode-normalization\",\"req\":\"^0.1\"},{\"name\":\"unicode-properties\",\"req\":\"^0.1.1\"}],\"features\":{}}",
|
||||
@@ -1371,6 +1375,8 @@
|
||||
"time-macros_0.2.27": "{\"dependencies\":[{\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"name\":\"time-core\",\"req\":\"=0.1.8\"}],\"features\":{\"formatting\":[],\"large-dates\":[],\"parsing\":[],\"serde\":[]}}",
|
||||
"time_0.3.47": "{\"dependencies\":[{\"default_features\":false,\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.8.1\",\"target\":\"cfg(bench)\"},{\"features\":[\"powerfmt\"],\"name\":\"deranged\",\"req\":\"^0.5.2\"},{\"name\":\"itoa\",\"optional\":true,\"req\":\"^1.0.1\"},{\"name\":\"js-sys\",\"optional\":true,\"req\":\"^0.3.58\",\"target\":\"cfg(all(target_family = \\\"wasm\\\", not(any(target_os = \\\"emscripten\\\", target_os = \\\"wasi\\\"))))\"},{\"name\":\"libc\",\"optional\":true,\"req\":\"^0.2.98\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"kind\":\"dev\",\"name\":\"num-conv\",\"req\":\"^0.2.0\"},{\"name\":\"num_threads\",\"optional\":true,\"req\":\"^0.1.2\",\"target\":\"cfg(target_family = \\\"unix\\\")\"},{\"default_features\":false,\"name\":\"powerfmt\",\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"quickcheck\",\"optional\":true,\"req\":\"^1.0.3\"},{\"kind\":\"dev\",\"name\":\"quickcheck_macros\",\"req\":\"^1.0.0\"},{\"default_features\":false,\"name\":\"rand08\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rand08\",\"package\":\"rand\",\"req\":\"^0.8.4\"},{\"default_features\":false,\"name\":\"rand09\",\"optional\":true,\"package\":\"rand\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand09\",\"package\":\"rand\",\"req\":\"^0.9.2\"},{\"default_features\":false,\"kind\":\"dev\",\"name\":\"rstest\",\"req\":\"^0.26.1\"},{\"kind\":\"dev\",\"name\":\"rstest_reuse\",\"req\":\"^0.7.0\"},{\"default_features\":false,\"features\":[\"derive\"],\"kind\":\"dev\",\"name\":\"serde\",\"req\":\"^1.0.184\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.68\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0.126\"},{\"name\":\"time-core\",\"req\":\"=0.1.8\"},{\"name\":\"time-macros\",\"optional\":true,\"req\":\"=0.2.27\"},{\"kind\":\"dev\",\"name\":\"time-macros\",\"req\":\"=0.2.27\"},{\"kind\":\"dev\",\"name\":\"trybuild\",\"req\":\"^1.0.102\",\"target\":\"cfg(__ui_tests)\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\"],\"default\":[\"std\"],\"formatting\":[\"dep:itoa\",\"std\",\"time-macros?/formatting\"],\"large-dates\":[\"time-core/large-dates\",\"time-macros?/large-dates\"],\"local-offset\":[\"std\",\"dep:libc\",\"dep:num_threads\"],\"macros\":[\"dep:time-macros\"],\"parsing\":[\"time-macros?/parsing\"],\"quickcheck\":[\"dep:quickcheck\",\"alloc\",\"deranged/quickcheck\"],\"rand\":[\"rand08\",\"rand09\"],\"rand08\":[\"dep:rand08\",\"deranged/rand08\"],\"rand09\":[\"dep:rand09\",\"deranged/rand09\"],\"serde\":[\"dep:serde_core\",\"time-macros?/serde\",\"deranged/serde\"],\"serde-human-readable\":[\"serde\",\"formatting\",\"parsing\"],\"serde-well-known\":[\"serde\",\"formatting\",\"parsing\"],\"std\":[\"alloc\"],\"wasm-bindgen\":[\"dep:js-sys\"]}}",
|
||||
"tiny-keccak_2.0.2": "{\"dependencies\":[{\"name\":\"crunchy\",\"req\":\"^0.2.2\"}],\"features\":{\"cshake\":[],\"default\":[],\"fips202\":[\"keccak\",\"shake\",\"sha3\"],\"k12\":[],\"keccak\":[],\"kmac\":[\"cshake\"],\"parallel_hash\":[\"cshake\"],\"sha3\":[],\"shake\":[],\"sp800\":[\"cshake\",\"kmac\",\"tuple_hash\"],\"tuple_hash\":[\"cshake\"]}}",
|
||||
"tiny-skia-path_0.11.4": "{\"dependencies\":[{\"name\":\"arrayref\",\"req\":\"^0.3.6\"},{\"name\":\"bytemuck\",\"req\":\"^1.4\"},{\"name\":\"libm\",\"optional\":true,\"req\":\"^0.2.1\"},{\"default_features\":false,\"name\":\"strict-num\",\"req\":\"^0.1\"}],\"features\":{\"default\":[\"std\"],\"no-std-float\":[\"libm\"],\"std\":[]}}",
|
||||
"tiny-skia_0.11.4": "{\"dependencies\":[{\"name\":\"arrayref\",\"req\":\"^0.3.6\"},{\"default_features\":false,\"name\":\"arrayvec\",\"req\":\"^0.7\"},{\"features\":[\"aarch64_simd\"],\"name\":\"bytemuck\",\"req\":\"^1.12\"},{\"name\":\"cfg-if\",\"req\":\"^1\"},{\"name\":\"log\",\"req\":\"^0.4\"},{\"name\":\"png\",\"optional\":true,\"req\":\"^0.17\"},{\"default_features\":false,\"name\":\"tiny-skia-path\",\"req\":\"^0.11.4\"}],\"features\":{\"default\":[\"std\",\"simd\",\"png-format\"],\"no-std-float\":[\"tiny-skia-path/no-std-float\"],\"png-format\":[\"std\",\"png\"],\"simd\":[],\"std\":[\"tiny-skia-path/std\"]}}",
|
||||
"tiny_http_0.12.0": "{\"dependencies\":[{\"name\":\"ascii\",\"req\":\"^1.0\"},{\"name\":\"chunked_transfer\",\"req\":\"^1\"},{\"kind\":\"dev\",\"name\":\"fdlimit\",\"req\":\"^0.1\"},{\"name\":\"httpdate\",\"req\":\"^1.0.2\"},{\"name\":\"log\",\"req\":\"^0.4.4\"},{\"name\":\"openssl\",\"optional\":true,\"req\":\"^0.10\"},{\"kind\":\"dev\",\"name\":\"rustc-serialize\",\"req\":\"^0.3\"},{\"name\":\"rustls\",\"optional\":true,\"req\":\"^0.20\"},{\"name\":\"rustls-pemfile\",\"optional\":true,\"req\":\"^0.2.1\"},{\"kind\":\"dev\",\"name\":\"sha1\",\"req\":\"^0.6.0\"},{\"name\":\"zeroize\",\"optional\":true,\"req\":\"^1\"}],\"features\":{\"default\":[],\"ssl\":[\"ssl-openssl\"],\"ssl-openssl\":[\"openssl\",\"zeroize\"],\"ssl-rustls\":[\"rustls\",\"rustls-pemfile\",\"zeroize\"]}}",
|
||||
"tinystr_0.8.2": "{\"dependencies\":[{\"kind\":\"dev\",\"name\":\"bincode\",\"req\":\"^1.3.1\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.5.0\",\"target\":\"cfg(not(target_arch = \\\"wasm32\\\"))\"},{\"default_features\":false,\"name\":\"databake\",\"optional\":true,\"req\":\"^0.2.0\"},{\"default_features\":false,\"name\":\"displaydoc\",\"req\":\"^0.2.3\"},{\"default_features\":false,\"features\":[\"use-std\"],\"kind\":\"dev\",\"name\":\"postcard\",\"req\":\"^1.0.3\"},{\"features\":[\"small_rng\"],\"kind\":\"dev\",\"name\":\"rand\",\"req\":\"^0.9\"},{\"default_features\":false,\"name\":\"serde_core\",\"optional\":true,\"req\":\"^1.0.220\"},{\"features\":[\"alloc\"],\"kind\":\"dev\",\"name\":\"serde_json\",\"req\":\"^1.0.45\"},{\"default_features\":false,\"name\":\"zerovec\",\"optional\":true,\"req\":\"^0.11.3\"}],\"features\":{\"alloc\":[\"serde_core?/alloc\",\"zerovec?/alloc\"],\"databake\":[\"dep:databake\"],\"default\":[\"alloc\"],\"serde\":[\"dep:serde_core\"],\"std\":[],\"zerovec\":[\"dep:zerovec\"]}}",
|
||||
"tinyvec_1.10.0": "{\"dependencies\":[{\"name\":\"arbitrary\",\"optional\":true,\"req\":\"^1\"},{\"default_features\":false,\"name\":\"borsh\",\"optional\":true,\"req\":\"^1.2.0\"},{\"kind\":\"dev\",\"name\":\"criterion\",\"req\":\"^0.3.0\"},{\"kind\":\"dev\",\"name\":\"debugger_test\",\"req\":\"^0.1\"},{\"kind\":\"dev\",\"name\":\"debugger_test_parser\",\"req\":\"^0.1\"},{\"default_features\":false,\"name\":\"generic-array\",\"optional\":true,\"req\":\"^1.1.1\"},{\"default_features\":false,\"name\":\"serde\",\"optional\":true,\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"serde_test\",\"req\":\"^1.0\"},{\"kind\":\"dev\",\"name\":\"smallvec\",\"req\":\"^1\"},{\"name\":\"tinyvec_macros\",\"optional\":true,\"req\":\"^0.1\"}],\"features\":{\"alloc\":[\"tinyvec_macros\"],\"debugger_visualizer\":[],\"default\":[],\"experimental_write_impl\":[],\"grab_spare_slice\":[],\"latest_stable_rust\":[\"rustc_1_61\"],\"nightly_slice_partition_dedup\":[],\"real_blackbox\":[\"criterion/real_blackbox\"],\"rustc_1_40\":[],\"rustc_1_55\":[],\"rustc_1_57\":[],\"rustc_1_61\":[\"rustc_1_57\"],\"std\":[\"alloc\"]}}",
|
||||
|
||||
61
codex-rs/Cargo.lock
generated
61
codex-rs/Cargo.lock
generated
@@ -459,6 +459,12 @@ dependencies = [
|
||||
"rustversion",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "arrayref"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb"
|
||||
|
||||
[[package]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.6"
|
||||
@@ -1545,6 +1551,7 @@ name = "codex-artifact-presentation"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"font8x8",
|
||||
"image",
|
||||
"ppt-rs",
|
||||
"pretty_assertions",
|
||||
@@ -1553,6 +1560,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tempfile",
|
||||
"thiserror 2.0.18",
|
||||
"tiny-skia",
|
||||
"tiny_http",
|
||||
"uuid",
|
||||
"zip 2.4.2",
|
||||
@@ -4088,6 +4096,12 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
|
||||
|
||||
[[package]]
|
||||
name = "font8x8"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e"
|
||||
|
||||
[[package]]
|
||||
name = "foreign-types"
|
||||
version = "0.3.2"
|
||||
@@ -5085,7 +5099,7 @@ dependencies = [
|
||||
"image-webp",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"png 0.18.0",
|
||||
"tiff",
|
||||
"zune-core 0.5.1",
|
||||
"zune-jpeg 0.5.12",
|
||||
@@ -7029,6 +7043,19 @@ dependencies = [
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.0"
|
||||
@@ -9512,6 +9539,12 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
||||
|
||||
[[package]]
|
||||
name = "strict-num"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
|
||||
|
||||
[[package]]
|
||||
name = "string_cache"
|
||||
version = "0.8.9"
|
||||
@@ -9991,6 +10024,32 @@ dependencies = [
|
||||
"crunchy",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "83d13394d44dae3207b52a326c0c85a8bf87f1541f23b0d143811088497b09ab"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"bytemuck",
|
||||
"cfg-if",
|
||||
"log",
|
||||
"png 0.17.16",
|
||||
"tiny-skia-path",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia-path"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9c9e7fc0c2e86a30b117d0462aa261b72b7a99b7ebd7deb3a14ceda95c5bdc93"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"bytemuck",
|
||||
"strict-num",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny_http"
|
||||
version = "0.12.0"
|
||||
|
||||
@@ -218,6 +218,8 @@ os_info = "3.12.0"
|
||||
owo-colors = "4.3.0"
|
||||
path-absolutize = "3.1.1"
|
||||
pathdiff = "0.2"
|
||||
font8x8 = "0.3.1"
|
||||
tiny-skia = "0.11.4"
|
||||
portable-pty = "0.9.0"
|
||||
ppt-rs = "0.2.6"
|
||||
predicates = "3"
|
||||
|
||||
@@ -13,11 +13,13 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
base64 = { workspace = true }
|
||||
font8x8 = { workspace = true }
|
||||
image = { workspace = true, features = ["jpeg", "png"] }
|
||||
ppt-rs = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["blocking"] }
|
||||
serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tiny-skia = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
uuid = { workspace = true, features = ["v4"] }
|
||||
zip = { workspace = true }
|
||||
|
||||
@@ -40,7 +40,6 @@ use std::io::Seek;
|
||||
use std::io::Write;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
use thiserror::Error;
|
||||
use uuid::Uuid;
|
||||
use zip::ZipArchive;
|
||||
@@ -75,6 +74,8 @@ pub enum PresentationArtifactError {
|
||||
ImportFailed { path: PathBuf, message: String },
|
||||
#[error("failed to export PPTX `{path}`: {message}")]
|
||||
ExportFailed { path: PathBuf, message: String },
|
||||
#[error("failed to render preview for action `{action}`: {message}")]
|
||||
RenderFailed { action: String, message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
|
||||
@@ -24,6 +24,13 @@ struct ExportPreviewArgs {
|
||||
quality: Option<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct RenderPreviewArgs {
|
||||
slide_index: Option<u32>,
|
||||
scale: Option<f32>,
|
||||
include_background: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
struct AddSlideArgs {
|
||||
layout: Option<String>,
|
||||
|
||||
@@ -100,6 +100,7 @@ impl PresentationArtifactManager {
|
||||
"import_pptx" => self.import_pptx(request, cwd),
|
||||
"export_pptx" => self.export_pptx(request, cwd),
|
||||
"export_preview" => self.export_preview(request, cwd),
|
||||
"render_preview" => self.render_preview(request),
|
||||
"get_summary" => self.get_summary(request),
|
||||
"list_slides" => self.list_slides(request),
|
||||
"list_layouts" => self.list_layouts(request),
|
||||
@@ -286,82 +287,39 @@ impl PresentationArtifactManager {
|
||||
parse_preview_output_format(args.format.as_deref(), &output_path, &request.action)?;
|
||||
let scale = normalize_preview_scale(args.scale, &request.action)?;
|
||||
let quality = normalize_preview_quality(args.quality, &request.action)?;
|
||||
if let Some(parent) = output_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: error.to_string(),
|
||||
let mut exported_paths = Vec::new();
|
||||
if let Some(slide_index) = args.slide_index {
|
||||
if let Some(parent) = output_path.parent() {
|
||||
std::fs::create_dir_all(parent).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let slide_index = usize::try_from(slide_index).map_err(|_| {
|
||||
PresentationArtifactError::InvalidArgs {
|
||||
action: request.action.clone(),
|
||||
message: "`slide_index` does not fit in usize".to_string(),
|
||||
}
|
||||
})?;
|
||||
}
|
||||
let temp_dir =
|
||||
std::env::temp_dir().join(format!("presentation_preview_{}", Uuid::new_v4().simple()));
|
||||
std::fs::create_dir_all(&temp_dir).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
let preview_document = if let Some(slide_index) = args.slide_index {
|
||||
let slide = document
|
||||
.slides
|
||||
.get(slide_index as usize)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
index_out_of_range(&request.action, slide_index as usize, document.slides.len())
|
||||
})?;
|
||||
let slide_id = slide.slide_id.clone();
|
||||
PresentationDocument {
|
||||
artifact_id: document.artifact_id.clone(),
|
||||
name: document.name.clone(),
|
||||
slide_size: document.slide_size,
|
||||
theme: document.theme.clone(),
|
||||
custom_text_styles: document.custom_text_styles.clone(),
|
||||
layouts: Vec::new(),
|
||||
slides: vec![slide],
|
||||
active_slide_index: Some(0),
|
||||
comment_self: document.comment_self.clone(),
|
||||
comment_threads: document
|
||||
.comment_threads
|
||||
.iter()
|
||||
.filter(|thread| match &thread.target {
|
||||
CommentTarget::Slide { slide_id: target_slide_id }
|
||||
| CommentTarget::Element { slide_id: target_slide_id, .. }
|
||||
| CommentTarget::TextRange { slide_id: target_slide_id, .. } => {
|
||||
target_slide_id == &slide_id
|
||||
}
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
next_slide_seq: 1,
|
||||
next_element_seq: 1,
|
||||
next_layout_seq: 1,
|
||||
next_text_range_seq: document.next_text_range_seq,
|
||||
next_comment_thread_seq: document.next_comment_thread_seq,
|
||||
next_comment_message_seq: document.next_comment_message_seq,
|
||||
}
|
||||
} else {
|
||||
document.clone()
|
||||
};
|
||||
write_preview_images(&preview_document, &temp_dir, &request.action)?;
|
||||
let mut exported_paths = collect_pngs(&temp_dir)?;
|
||||
if args.slide_index.is_some() {
|
||||
let rendered =
|
||||
exported_paths
|
||||
.pop()
|
||||
.ok_or_else(|| PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: "preview renderer produced no images".to_string(),
|
||||
})?;
|
||||
write_preview_image(
|
||||
&rendered,
|
||||
let png_bytes = render_slide_png(
|
||||
document,
|
||||
slide_index,
|
||||
RenderOptions {
|
||||
scale: 1.0,
|
||||
include_background: true,
|
||||
},
|
||||
)?;
|
||||
write_preview_image_bytes(
|
||||
&png_bytes,
|
||||
&output_path,
|
||||
preview_format,
|
||||
scale,
|
||||
quality,
|
||||
&request.action,
|
||||
)?;
|
||||
exported_paths = vec![output_path];
|
||||
exported_paths.push(output_path);
|
||||
} else {
|
||||
std::fs::create_dir_all(&output_path).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
@@ -369,30 +327,27 @@ impl PresentationArtifactManager {
|
||||
message: error.to_string(),
|
||||
}
|
||||
})?;
|
||||
let mut relocated = Vec::new();
|
||||
for rendered in exported_paths {
|
||||
let filename = rendered.file_name().ok_or_else(|| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: output_path.clone(),
|
||||
message: "rendered preview had no filename".to_string(),
|
||||
}
|
||||
})?;
|
||||
let stem = Path::new(filename)
|
||||
.file_stem()
|
||||
.and_then(|value| value.to_str())
|
||||
.unwrap_or("preview");
|
||||
let target = output_path.join(format!("{stem}.{}", preview_format.extension()));
|
||||
write_preview_image(
|
||||
&rendered,
|
||||
for slide_index in 0..document.slides.len() {
|
||||
let png_bytes = render_slide_png(
|
||||
document,
|
||||
slide_index,
|
||||
RenderOptions {
|
||||
scale: 1.0,
|
||||
include_background: true,
|
||||
},
|
||||
)?;
|
||||
let target =
|
||||
output_path.join(format!("slide-{}.{}", slide_index + 1, preview_format.extension()));
|
||||
write_preview_image_bytes(
|
||||
&png_bytes,
|
||||
&target,
|
||||
preview_format,
|
||||
scale,
|
||||
quality,
|
||||
&request.action,
|
||||
)?;
|
||||
relocated.push(target);
|
||||
exported_paths.push(target);
|
||||
}
|
||||
exported_paths = relocated;
|
||||
}
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
@@ -404,6 +359,36 @@ impl PresentationArtifactManager {
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn render_preview(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
) -> Result<PresentationArtifactResponse, PresentationArtifactError> {
|
||||
let args: RenderPreviewArgs = parse_args(&request.action, &request.args)?;
|
||||
let artifact_id = required_artifact_id(&request)?;
|
||||
let document = self.get_document(&artifact_id, &request.action)?;
|
||||
let slide_index = resolve_render_slide_index(document, args.slide_index, &request.action)?;
|
||||
let png_bytes = render_slide_png(
|
||||
document,
|
||||
slide_index,
|
||||
RenderOptions {
|
||||
scale: normalize_preview_scale(args.scale, &request.action)?,
|
||||
include_background: args.include_background.unwrap_or(true),
|
||||
},
|
||||
)?;
|
||||
let mut response = PresentationArtifactResponse::new(
|
||||
artifact_id,
|
||||
request.action,
|
||||
format!("Rendered preview for slide {}", slide_index + 1),
|
||||
snapshot_for_document(document),
|
||||
);
|
||||
response.rendered_preview = Some(RenderedPreview {
|
||||
slide_index,
|
||||
png_bytes,
|
||||
});
|
||||
response.active_slide_index = document.active_slide_index;
|
||||
Ok(response)
|
||||
}
|
||||
|
||||
fn get_summary(
|
||||
&mut self,
|
||||
request: PresentationArtifactRequest,
|
||||
@@ -3031,6 +3016,7 @@ impl PresentationArtifactManager {
|
||||
proto_json: None,
|
||||
patch: None,
|
||||
active_slide_index: None,
|
||||
rendered_preview: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -8,3 +8,4 @@ include!("proto.rs");
|
||||
include!("inspect.rs");
|
||||
include!("pptx.rs");
|
||||
include!("snapshot.rs");
|
||||
include!("render.rs");
|
||||
|
||||
@@ -33,6 +33,7 @@ fn is_read_only_action(action: &str) -> bool {
|
||||
| "get_style"
|
||||
| "describe_styles"
|
||||
| "record_patch"
|
||||
| "render_preview"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -40,7 +41,7 @@ fn tracks_history(action: &str) -> bool {
|
||||
!is_read_only_action(action)
|
||||
&& !matches!(
|
||||
action,
|
||||
"export_pptx" | "export_preview" | "undo" | "redo" | "apply_patch"
|
||||
"export_pptx" | "export_preview" | "render_preview" | "undo" | "redo" | "apply_patch"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -715,94 +715,8 @@ fn slide_table_xml(slide: &PresentationSlide) -> String {
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn write_preview_images(
|
||||
document: &PresentationDocument,
|
||||
output_dir: &Path,
|
||||
action: &str,
|
||||
) -> Result<(), PresentationArtifactError> {
|
||||
let pptx_path = output_dir.join("preview.pptx");
|
||||
let bytes = build_pptx_bytes(document, action).map_err(|message| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.clone(),
|
||||
message,
|
||||
}
|
||||
})?;
|
||||
std::fs::write(&pptx_path, bytes).map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.clone(),
|
||||
message: error.to_string(),
|
||||
})?;
|
||||
render_pptx_to_pngs(&pptx_path, output_dir, action)
|
||||
}
|
||||
|
||||
fn render_pptx_to_pngs(
|
||||
pptx_path: &Path,
|
||||
output_dir: &Path,
|
||||
action: &str,
|
||||
) -> Result<(), PresentationArtifactError> {
|
||||
let soffice_cmd = if cfg!(target_os = "macos")
|
||||
&& Path::new("/Applications/LibreOffice.app/Contents/MacOS/soffice").exists()
|
||||
{
|
||||
"/Applications/LibreOffice.app/Contents/MacOS/soffice"
|
||||
} else {
|
||||
"soffice"
|
||||
};
|
||||
let conversion = Command::new(soffice_cmd)
|
||||
.arg("--headless")
|
||||
.arg("--convert-to")
|
||||
.arg("pdf")
|
||||
.arg(pptx_path)
|
||||
.arg("--outdir")
|
||||
.arg(output_dir)
|
||||
.output()
|
||||
.map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.to_path_buf(),
|
||||
message: format!("{action}: failed to execute LibreOffice: {error}"),
|
||||
})?;
|
||||
if !conversion.status.success() {
|
||||
return Err(PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.to_path_buf(),
|
||||
message: format!(
|
||||
"{action}: LibreOffice conversion failed: {}",
|
||||
String::from_utf8_lossy(&conversion.stderr)
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
let pdf_path = output_dir.join(
|
||||
pptx_path
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.map(|stem| format!("{stem}.pdf"))
|
||||
.ok_or_else(|| PresentationArtifactError::ExportFailed {
|
||||
path: pptx_path.to_path_buf(),
|
||||
message: format!("{action}: preview pptx filename is invalid"),
|
||||
})?,
|
||||
);
|
||||
let prefix = output_dir.join("slide");
|
||||
let conversion = Command::new("pdftoppm")
|
||||
.arg("-png")
|
||||
.arg(&pdf_path)
|
||||
.arg(&prefix)
|
||||
.output()
|
||||
.map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: pdf_path.clone(),
|
||||
message: format!("{action}: failed to execute pdftoppm: {error}"),
|
||||
})?;
|
||||
std::fs::remove_file(&pdf_path).ok();
|
||||
if !conversion.status.success() {
|
||||
return Err(PresentationArtifactError::ExportFailed {
|
||||
path: output_dir.to_path_buf(),
|
||||
message: format!(
|
||||
"{action}: pdftoppm conversion failed: {}",
|
||||
String::from_utf8_lossy(&conversion.stderr)
|
||||
),
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_preview_image(
|
||||
source_path: &Path,
|
||||
pub(crate) fn write_preview_image_bytes(
|
||||
png_bytes: &[u8],
|
||||
target_path: &Path,
|
||||
format: PreviewOutputFormat,
|
||||
scale: f32,
|
||||
@@ -810,7 +724,7 @@ pub(crate) fn write_preview_image(
|
||||
action: &str,
|
||||
) -> Result<(), PresentationArtifactError> {
|
||||
if matches!(format, PreviewOutputFormat::Png) && scale == 1.0 {
|
||||
std::fs::rename(source_path, target_path).map_err(|error| {
|
||||
std::fs::write(target_path, png_bytes).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: target_path.to_path_buf(),
|
||||
message: error.to_string(),
|
||||
@@ -818,11 +732,12 @@ pub(crate) fn write_preview_image(
|
||||
})?;
|
||||
return Ok(());
|
||||
}
|
||||
let mut preview =
|
||||
image::open(source_path).map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: source_path.to_path_buf(),
|
||||
let mut preview = image::load_from_memory(png_bytes).map_err(|error| {
|
||||
PresentationArtifactError::ExportFailed {
|
||||
path: target_path.to_path_buf(),
|
||||
message: format!("{action}: {error}"),
|
||||
})?;
|
||||
}
|
||||
})?;
|
||||
if scale != 1.0 {
|
||||
let width = (preview.width() as f32 * scale).round().max(1.0) as u32;
|
||||
let height = (preview.height() as f32 * scale).round().max(1.0) as u32;
|
||||
@@ -880,24 +795,9 @@ pub(crate) fn write_preview_image(
|
||||
})?;
|
||||
}
|
||||
}
|
||||
std::fs::remove_file(source_path).ok();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_pngs(output_dir: &Path) -> Result<Vec<PathBuf>, PresentationArtifactError> {
|
||||
let mut files = std::fs::read_dir(output_dir)
|
||||
.map_err(|error| PresentationArtifactError::ExportFailed {
|
||||
path: output_dir.to_path_buf(),
|
||||
message: error.to_string(),
|
||||
})?
|
||||
.filter_map(Result::ok)
|
||||
.map(|entry| entry.path())
|
||||
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("png"))
|
||||
.collect::<Vec<_>>();
|
||||
files.sort();
|
||||
Ok(files)
|
||||
}
|
||||
|
||||
fn parse_preview_output_format(
|
||||
format: Option<&str>,
|
||||
path: &Path,
|
||||
|
||||
1854
codex-rs/artifact-presentation/src/presentation_artifact/render.rs
Normal file
1854
codex-rs/artifact-presentation/src/presentation_artifact/render.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -27,6 +27,14 @@ pub struct PresentationArtifactResponse {
|
||||
pub patch: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub active_slide_index: Option<usize>,
|
||||
#[serde(skip)]
|
||||
pub rendered_preview: Option<RenderedPreview>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RenderedPreview {
|
||||
pub slide_index: usize,
|
||||
pub png_bytes: Vec<u8>,
|
||||
}
|
||||
|
||||
impl PresentationArtifactResponse {
|
||||
@@ -52,6 +60,7 @@ impl PresentationArtifactResponse {
|
||||
proto_json: None,
|
||||
patch: None,
|
||||
active_slide_index: None,
|
||||
rendered_preview: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -78,6 +87,7 @@ fn response_for_document_state(
|
||||
proto_json: None,
|
||||
patch: None,
|
||||
active_slide_index: document.and_then(|current| current.active_slide_index),
|
||||
rendered_preview: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::presentation_artifact::*;
|
||||
use base64::Engine;
|
||||
use image::GenericImageView;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::io::Read;
|
||||
|
||||
@@ -29,6 +30,28 @@ fn parse_ndjson_lines(ndjson: &str) -> Result<Vec<serde_json::Value>, serde_json
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn load_preview(bytes: &[u8]) -> Result<image::DynamicImage, Box<dyn std::error::Error>> {
|
||||
Ok(image::load_from_memory(bytes)?)
|
||||
}
|
||||
|
||||
fn rect_contains_pixel(
|
||||
image: &image::DynamicImage,
|
||||
left: u32,
|
||||
top: u32,
|
||||
width: u32,
|
||||
height: u32,
|
||||
expected: [u8; 4],
|
||||
) -> bool {
|
||||
for y in top..top.saturating_add(height) {
|
||||
for x in left..left.saturating_add(width) {
|
||||
if image.get_pixel(x, y).0 == expected {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn manager_can_create_add_text_and_export() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
@@ -509,12 +532,16 @@ fn image_fit_contain_preserves_aspect_ratio() {
|
||||
#[test]
|
||||
fn preview_image_writer_supports_jpeg_scale_and_svg() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let source_path = temp_dir.path().join("preview.png");
|
||||
image::RgbaImage::from_pixel(80, 40, image::Rgba([0x22, 0x66, 0xAA, 0xFF]))
|
||||
.save(&source_path)?;
|
||||
let mut source_bytes = std::io::Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||
80,
|
||||
40,
|
||||
image::Rgba([0x22, 0x66, 0xAA, 0xFF]),
|
||||
))
|
||||
.write_to(&mut source_bytes, image::ImageFormat::Png)?;
|
||||
let target_path = temp_dir.path().join("preview.jpg");
|
||||
write_preview_image(
|
||||
&source_path,
|
||||
write_preview_image_bytes(
|
||||
&source_bytes.into_inner(),
|
||||
&target_path,
|
||||
PreviewOutputFormat::Jpeg,
|
||||
0.5,
|
||||
@@ -527,14 +554,17 @@ fn preview_image_writer_supports_jpeg_scale_and_svg() -> Result<(), Box<dyn std:
|
||||
image::ImageFormat::from_path(&target_path)?,
|
||||
image::ImageFormat::Jpeg
|
||||
);
|
||||
assert!(!source_path.exists());
|
||||
|
||||
let svg_source_path = temp_dir.path().join("preview-svg.png");
|
||||
image::RgbaImage::from_pixel(32, 16, image::Rgba([0x55, 0xAA, 0x44, 0xFF]))
|
||||
.save(&svg_source_path)?;
|
||||
let mut svg_source_bytes = std::io::Cursor::new(Vec::new());
|
||||
image::DynamicImage::ImageRgba8(image::RgbaImage::from_pixel(
|
||||
32,
|
||||
16,
|
||||
image::Rgba([0x55, 0xAA, 0x44, 0xFF]),
|
||||
))
|
||||
.write_to(&mut svg_source_bytes, image::ImageFormat::Png)?;
|
||||
let svg_target_path = temp_dir.path().join("preview.svg");
|
||||
write_preview_image(
|
||||
&svg_source_path,
|
||||
write_preview_image_bytes(
|
||||
&svg_source_bytes.into_inner(),
|
||||
&svg_target_path,
|
||||
PreviewOutputFormat::Svg,
|
||||
0.5,
|
||||
@@ -544,7 +574,6 @@ fn preview_image_writer_supports_jpeg_scale_and_svg() -> Result<(), Box<dyn std:
|
||||
let svg = std::fs::read_to_string(&svg_target_path)?;
|
||||
assert!(svg.contains(r#"<svg xmlns="http://www.w3.org/2000/svg" width="16" height="8""#));
|
||||
assert!(svg.contains("data:image/png;base64,"));
|
||||
assert!(!svg_source_path.exists());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3510,3 +3539,341 @@ fn proto_and_patch_actions_work_and_patch_history_is_atomic()
|
||||
assert_eq!(redone_proto.proto_json, Some(proto));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_preview_returns_png_for_active_slide() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({
|
||||
"name": "Preview",
|
||||
"theme": {
|
||||
"color_scheme": {
|
||||
"bg1": "F7F4EA",
|
||||
"tx1": "1E1E1E"
|
||||
}
|
||||
}
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({
|
||||
"background_fill": "#EAE4D5"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"geometry": "rectangle",
|
||||
"position": { "left": 80, "top": 110, "width": 180, "height": 90 },
|
||||
"fill": "#CC5533",
|
||||
"text": "Preview card",
|
||||
"text_style": { "color": "#FFFFFF", "alignment": "center", "bold": true }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_text_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"text": "Quarterly update",
|
||||
"position": { "left": 72, "top": 40, "width": 320, "height": 44 },
|
||||
"style": "title"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let response = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "render_preview".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let preview = response.rendered_preview.expect("rendered preview present");
|
||||
let rendered = load_preview(&preview.png_bytes)?;
|
||||
|
||||
assert_eq!(preview.slide_index, 0);
|
||||
assert_eq!(rendered.dimensions(), (720, 540));
|
||||
assert_eq!(rendered.get_pixel(20, 20).0, [0xEA, 0xE4, 0xD5, 0xFF]);
|
||||
assert_eq!(rendered.get_pixel(120, 150).0, [0xCC, 0x55, 0x33, 0xFF]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_preview_renders_image_and_table_content() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let image_path = temp_dir.path().join("preview-source.png");
|
||||
image::RgbaImage::from_pixel(40, 30, image::Rgba([0x2A, 0x80, 0xD7, 0xFF]))
|
||||
.save(&image_path)?;
|
||||
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Image and table" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_image".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"path": image_path,
|
||||
"position": { "left": 40, "top": 40, "width": 160, "height": 120 },
|
||||
"fit": "contain"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_table".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"position": { "left": 240, "top": 60, "width": 220, "height": 120 },
|
||||
"rows": [
|
||||
["Metric", "Value"],
|
||||
["ARR", "$9.2M"]
|
||||
],
|
||||
"style_options": { "header_row": true }
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let response = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "render_preview".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let preview = load_preview(
|
||||
&response
|
||||
.rendered_preview
|
||||
.expect("rendered preview present")
|
||||
.png_bytes,
|
||||
)?;
|
||||
|
||||
assert_eq!(preview.get_pixel(80, 80).0, [0x2A, 0x80, 0xD7, 0xFF]);
|
||||
assert_eq!(preview.get_pixel(270, 70).0, [0xEA, 0xF0, 0xF8, 0xFF]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_preview_renders_chart_primitives() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Chart preview" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_chart".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"chart_type": "bar",
|
||||
"position": { "left": 120, "top": 80, "width": 360, "height": 220 },
|
||||
"title": "Pipeline",
|
||||
"categories": ["Q1", "Q2", "Q3"],
|
||||
"series": [
|
||||
{ "name": "Actual", "values": [5, 7, 9], "fill": "#4E79A7" }
|
||||
]
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let response = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "render_preview".to_string(),
|
||||
args: serde_json::json!({}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let preview = load_preview(
|
||||
&response
|
||||
.rendered_preview
|
||||
.expect("rendered preview present")
|
||||
.png_bytes,
|
||||
)?;
|
||||
|
||||
assert!(
|
||||
rect_contains_pixel(&preview, 150, 120, 220, 180, [0x4E, 0x79, 0xA7, 0xFF]),
|
||||
"expected to find the series fill color within the chart plot area"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_preview_uses_native_renderer_for_single_slide() -> Result<(), Box<dyn std::error::Error>>
|
||||
{
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Export preview" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({
|
||||
"background_fill": "#F2E8DC"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_shape".to_string(),
|
||||
args: serde_json::json!({
|
||||
"slide_index": 0,
|
||||
"geometry": "rectangle",
|
||||
"position": { "left": 96, "top": 120, "width": 160, "height": 80 },
|
||||
"fill": "#2F6B5F"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let target_path = temp_dir.path().join("preview.png");
|
||||
let response = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "export_preview".to_string(),
|
||||
args: serde_json::json!({
|
||||
"path": target_path,
|
||||
"slide_index": 0
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let exported_path = response
|
||||
.exported_paths
|
||||
.first()
|
||||
.expect("exported preview path present");
|
||||
let preview = image::open(exported_path)?;
|
||||
|
||||
assert_eq!(response.exported_paths.len(), 1);
|
||||
assert_eq!(preview.dimensions(), (720, 540));
|
||||
assert_eq!(preview.get_pixel(20, 20).0, [0xF2, 0xE8, 0xDC, 0xFF]);
|
||||
assert_eq!(preview.get_pixel(120, 150).0, [0x2F, 0x6B, 0x5F, 0xFF]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn export_preview_writes_all_slides_with_stable_names() -> Result<(), Box<dyn std::error::Error>> {
|
||||
let temp_dir = tempfile::tempdir()?;
|
||||
let mut manager = PresentationArtifactManager::default();
|
||||
let created = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: None,
|
||||
action: "create".to_string(),
|
||||
args: serde_json::json!({ "name": "Export all previews" }),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
let artifact_id = created.artifact_id;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({
|
||||
"background_fill": "#FFF4CC"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id.clone()),
|
||||
action: "add_slide".to_string(),
|
||||
args: serde_json::json!({
|
||||
"background_fill": "#D9EAF7"
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
let output_dir = temp_dir.path().join("previews");
|
||||
let response = manager.execute(
|
||||
PresentationArtifactRequest {
|
||||
artifact_id: Some(artifact_id),
|
||||
action: "export_preview".to_string(),
|
||||
args: serde_json::json!({
|
||||
"path": output_dir
|
||||
}),
|
||||
},
|
||||
temp_dir.path(),
|
||||
)?;
|
||||
|
||||
assert_eq!(response.exported_paths.len(), 2);
|
||||
assert_eq!(
|
||||
response.exported_paths,
|
||||
vec![
|
||||
temp_dir.path().join("previews/slide-1.png"),
|
||||
temp_dir.path().join("previews/slide-2.png"),
|
||||
]
|
||||
);
|
||||
let slide_1 = image::open(&response.exported_paths[0])?;
|
||||
let slide_2 = image::open(&response.exported_paths[1])?;
|
||||
assert_eq!(slide_1.get_pixel(20, 20).0, [0xFF, 0xF4, 0xCC, 0xFF]);
|
||||
assert_eq!(slide_2.get_pixel(20, 20).0, [0xD9, 0xEA, 0xF7, 0xFF]);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
use async_trait::async_trait;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_artifact_presentation::PathAccessKind;
|
||||
use codex_artifact_presentation::PathAccessRequirement;
|
||||
use codex_artifact_presentation::PresentationArtifactError;
|
||||
use codex_artifact_presentation::PresentationArtifactToolRequest;
|
||||
use codex_protocol::models::FunctionCallOutputContentItem;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use serde_json::to_string;
|
||||
@@ -68,6 +72,19 @@ impl ToolHandler for PresentationArtifactHandler {
|
||||
};
|
||||
|
||||
let request: PresentationArtifactToolRequest = parse_arguments(&arguments)?;
|
||||
if request
|
||||
.actions
|
||||
.iter()
|
||||
.any(|action| action.action == "render_preview")
|
||||
&& !turn
|
||||
.model_info
|
||||
.input_modalities
|
||||
.contains(&InputModality::Image)
|
||||
{
|
||||
return Err(FunctionCallError::RespondToModel(
|
||||
"render_preview is not allowed because you do not support image inputs".to_string(),
|
||||
));
|
||||
}
|
||||
for access in request
|
||||
.required_path_accesses(&turn.cwd)
|
||||
.map_err(presentation_error)?
|
||||
@@ -85,6 +102,26 @@ impl ToolHandler for PresentationArtifactHandler {
|
||||
.await
|
||||
.map_err(presentation_error)?;
|
||||
|
||||
if let Some(preview) = response.rendered_preview.as_ref() {
|
||||
let summary = format!(
|
||||
"{} Artifact `{}` slide {}.",
|
||||
response.summary,
|
||||
response.artifact_id,
|
||||
preview.slide_index + 1
|
||||
);
|
||||
let image_url = format!(
|
||||
"data:image/png;base64,{}",
|
||||
BASE64_STANDARD.encode(&preview.png_bytes)
|
||||
);
|
||||
return Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::ContentItems(vec![
|
||||
FunctionCallOutputContentItem::InputText { text: summary },
|
||||
FunctionCallOutputContentItem::InputImage { image_url },
|
||||
]),
|
||||
success: Some(true),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(ToolOutput::Function {
|
||||
body: FunctionCallOutputBody::Text(to_string(&response).map_err(|error| {
|
||||
FunctionCallError::RespondToModel(format!(
|
||||
|
||||
@@ -198,3 +198,10 @@ Example preview:
|
||||
|
||||
Example JPEG preview:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"export_preview","args":{"slide_index":0,"path":"artifacts/q2-update-slide1.jpg","format":"jpeg","scale":0.75,"quality":85}}]}`
|
||||
|
||||
For model-visible previews, use `render_preview`. It renders a slide natively and returns inline image output instead of writing a file.
|
||||
|
||||
Example inline preview:
|
||||
`{"artifact_id":"presentation_x","actions":[{"action":"render_preview","args":{"slide_index":0,"scale":1.0}}]}`
|
||||
|
||||
`render_preview` defaults to the active slide when `slide_index` is omitted. It supports `scale` and `include_background`.
|
||||
|
||||
@@ -96,6 +96,7 @@ mod permissions_messages;
|
||||
mod personality;
|
||||
mod personality_migration;
|
||||
mod plugins;
|
||||
mod presentation_artifact;
|
||||
mod prompt_caching;
|
||||
mod quota_exceeded;
|
||||
mod read_file;
|
||||
|
||||
278
codex-rs/core/tests/suite/presentation_artifact.rs
Normal file
278
codex-rs/core/tests/suite/presentation_artifact.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
#![cfg(not(target_os = "windows"))]
|
||||
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::STANDARD as BASE64_STANDARD;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_protocol::config_types::ReasoningSummary;
|
||||
use codex_protocol::openai_models::ConfigShellToolType;
|
||||
use codex_protocol::openai_models::InputModality;
|
||||
use codex_protocol::openai_models::ModelInfo;
|
||||
use codex_protocol::openai_models::ModelVisibility;
|
||||
use codex_protocol::openai_models::ModelsResponse;
|
||||
use codex_protocol::openai_models::ReasoningEffort;
|
||||
use codex_protocol::openai_models::ReasoningEffortPreset;
|
||||
use codex_protocol::openai_models::TruncationPolicyConfig;
|
||||
use codex_protocol::protocol::AskForApproval;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::SandboxPolicy;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use core_test_support::responses;
|
||||
use core_test_support::responses::ev_assistant_message;
|
||||
use core_test_support::responses::ev_completed;
|
||||
use core_test_support::responses::ev_function_call;
|
||||
use core_test_support::responses::ev_response_created;
|
||||
use core_test_support::responses::mount_models_once;
|
||||
use core_test_support::responses::sse;
|
||||
use core_test_support::responses::start_mock_server;
|
||||
use core_test_support::skip_if_no_network;
|
||||
use core_test_support::test_codex::TestCodex;
|
||||
use core_test_support::test_codex::test_codex;
|
||||
use core_test_support::wait_for_event;
|
||||
use image::GenericImageView;
|
||||
use image::load_from_memory;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::Value;
|
||||
use wiremock::BodyPrintLimit;
|
||||
use wiremock::MockServer;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn presentation_artifact_render_preview_returns_inline_image() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = start_mock_server().await;
|
||||
let TestCodex {
|
||||
codex,
|
||||
cwd,
|
||||
session_configured,
|
||||
..
|
||||
} = test_codex().build(&server).await?;
|
||||
|
||||
let call_id = "presentation-render-preview";
|
||||
let arguments = serde_json::json!({
|
||||
"actions": [
|
||||
{
|
||||
"action": "create",
|
||||
"args": {
|
||||
"name": "Preview",
|
||||
"theme": {
|
||||
"color_scheme": {
|
||||
"bg1": "F6F2E8",
|
||||
"tx1": "1B1B1B"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add_slide",
|
||||
"args": {
|
||||
"background_fill": "#F1E6D6"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "add_shape",
|
||||
"args": {
|
||||
"slide_index": 0,
|
||||
"geometry": "rectangle",
|
||||
"position": { "left": 72, "top": 120, "width": 160, "height": 80 },
|
||||
"fill": "#C95A3D"
|
||||
}
|
||||
},
|
||||
{
|
||||
"action": "render_preview",
|
||||
"args": {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "presentation_artifact", &arguments),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "render the deck preview".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: session_configured.model.clone(),
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let function_output = mock.single_request().function_call_output(call_id);
|
||||
let output_items = function_output
|
||||
.get("output")
|
||||
.and_then(Value::as_array)
|
||||
.expect("render_preview output should be content items");
|
||||
assert_eq!(output_items.len(), 2);
|
||||
assert_eq!(
|
||||
output_items[0].get("type").and_then(Value::as_str),
|
||||
Some("input_text")
|
||||
);
|
||||
assert_eq!(
|
||||
output_items[1].get("type").and_then(Value::as_str),
|
||||
Some("input_image")
|
||||
);
|
||||
let image_url = output_items[1]
|
||||
.get("image_url")
|
||||
.and_then(Value::as_str)
|
||||
.expect("preview image_url present");
|
||||
let (prefix, payload) = image_url
|
||||
.split_once(',')
|
||||
.expect("preview image contains data prefix");
|
||||
assert_eq!(prefix, "data:image/png;base64");
|
||||
|
||||
let decoded = BASE64_STANDARD.decode(payload)?;
|
||||
let rendered = load_from_memory(&decoded)?;
|
||||
assert_eq!(rendered.dimensions(), (720, 540));
|
||||
assert_eq!(rendered.get_pixel(20, 20).0, [0xF1, 0xE6, 0xD6, 0xFF]);
|
||||
assert_eq!(rendered.get_pixel(100, 150).0, [0xC9, 0x5A, 0x3D, 0xFF]);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn presentation_artifact_render_preview_fails_for_text_only_model() -> anyhow::Result<()> {
|
||||
skip_if_no_network!(Ok(()));
|
||||
|
||||
let server = MockServer::builder()
|
||||
.body_print_limit(BodyPrintLimit::Limited(80_000))
|
||||
.start()
|
||||
.await;
|
||||
let model_slug = "text-only-presentation-preview-test-model";
|
||||
let text_only_model = ModelInfo {
|
||||
slug: model_slug.to_string(),
|
||||
display_name: "Text-only presentation preview test model".to_string(),
|
||||
description: Some(
|
||||
"Remote model for presentation preview unsupported-path coverage".to_string(),
|
||||
),
|
||||
default_reasoning_level: Some(ReasoningEffort::Medium),
|
||||
supported_reasoning_levels: vec![ReasoningEffortPreset {
|
||||
effort: ReasoningEffort::Medium,
|
||||
description: ReasoningEffort::Medium.to_string(),
|
||||
}],
|
||||
shell_type: ConfigShellToolType::ShellCommand,
|
||||
visibility: ModelVisibility::List,
|
||||
supported_in_api: true,
|
||||
input_modalities: vec![InputModality::Text],
|
||||
prefer_websockets: false,
|
||||
used_fallback_model_metadata: false,
|
||||
priority: 1,
|
||||
upgrade: None,
|
||||
base_instructions: "base instructions".to_string(),
|
||||
model_messages: None,
|
||||
supports_reasoning_summaries: false,
|
||||
default_reasoning_summary: ReasoningSummary::Auto,
|
||||
support_verbosity: false,
|
||||
default_verbosity: None,
|
||||
availability_nux: None,
|
||||
apply_patch_tool_type: None,
|
||||
truncation_policy: TruncationPolicyConfig::bytes(10_000),
|
||||
supports_parallel_tool_calls: false,
|
||||
context_window: Some(272_000),
|
||||
auto_compact_token_limit: None,
|
||||
effective_context_window_percent: 95,
|
||||
experimental_supported_tools: Vec::new(),
|
||||
};
|
||||
mount_models_once(
|
||||
&server,
|
||||
ModelsResponse {
|
||||
models: vec![text_only_model],
|
||||
},
|
||||
)
|
||||
.await;
|
||||
|
||||
let TestCodex { codex, cwd, .. } = test_codex()
|
||||
.with_auth(CodexAuth::create_dummy_chatgpt_auth_for_testing())
|
||||
.with_config(|config| {
|
||||
config.model = Some(model_slug.to_string());
|
||||
})
|
||||
.build(&server)
|
||||
.await?;
|
||||
|
||||
let call_id = "presentation-render-preview-unsupported";
|
||||
let arguments = serde_json::json!({
|
||||
"actions": [
|
||||
{
|
||||
"action": "create",
|
||||
"args": { "name": "Preview" }
|
||||
},
|
||||
{
|
||||
"action": "add_slide",
|
||||
"args": {}
|
||||
},
|
||||
{
|
||||
"action": "render_preview",
|
||||
"args": {}
|
||||
}
|
||||
]
|
||||
})
|
||||
.to_string();
|
||||
|
||||
let first_response = sse(vec![
|
||||
ev_response_created("resp-1"),
|
||||
ev_function_call(call_id, "presentation_artifact", &arguments),
|
||||
ev_completed("resp-1"),
|
||||
]);
|
||||
responses::mount_sse_once(&server, first_response).await;
|
||||
|
||||
let second_response = sse(vec![
|
||||
ev_assistant_message("msg-1", "done"),
|
||||
ev_completed("resp-2"),
|
||||
]);
|
||||
let mock = responses::mount_sse_once(&server, second_response).await;
|
||||
|
||||
codex
|
||||
.submit(Op::UserTurn {
|
||||
items: vec![UserInput::Text {
|
||||
text: "render the deck preview".into(),
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
final_output_json_schema: None,
|
||||
cwd: cwd.path().to_path_buf(),
|
||||
approval_policy: AskForApproval::Never,
|
||||
sandbox_policy: SandboxPolicy::DangerFullAccess,
|
||||
model: model_slug.to_string(),
|
||||
effort: None,
|
||||
summary: None,
|
||||
service_tier: None,
|
||||
collaboration_mode: None,
|
||||
personality: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
wait_for_event(&codex, |event| matches!(event, EventMsg::TurnComplete(_))).await;
|
||||
|
||||
let output_text = mock
|
||||
.single_request()
|
||||
.function_call_output_content_and_success(call_id)
|
||||
.and_then(|(content, _)| content)
|
||||
.expect("output text present");
|
||||
assert_eq!(
|
||||
output_text,
|
||||
"render_preview is not allowed because you do not support image inputs"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user