当前位置: 首页 > article >正文

Tauri2+Leptos开发桌面应用--绘制图形、制作GIF动画和mp4视频

在之前工作(Tauri2+Leptos开发桌面应用--新建窗口、自定义菜单和多页面切换_tauri实现多窗口-CSDN博客)的基础上继续尝试绘制图形、制作GIF动画和mp4视频

绘制图形主要尝试了两种方式,一种是调用python使用matplotlib模块绘制图形,一种是使用纯Rust开发的图形库Plotters来绘制图形,包括png、svg格式的图片,gif格式的动画。制作mp4视频主要是使用Plotters结合video_rs来完成的。

上述功能都是写成Tauri后台命令,通过前端Leptos调用显示实现的。

一、Leptos前端

为了方便,在主窗口单独新建了一个页面用于绘图练习。

1. main.rs文件
mod app;

use app::*;
use leptos::prelude::*;


//打开trunk serve --open 以开始开发您的应用程序。 Trunk 服务器将在文件更改时重新加载您的应用程序,从而使开发相对无缝。

fn main() {
    console_error_panic_hook::set_once();   //浏览器中运行 WASM 代码发生 panic 时可以获得一个实际的 Rust 堆栈跟踪,其中包括 Rust 源代码中的一行。
    mount_to_body(|| {
        view! {
            <App />
        }
    })
}

main.rs文件中使用了模块app,app.rs文件主要为导航页面。

2. app.rs文件
#[warn(unused_imports)]
use leptos::prelude::*;
use leptos_router::components::{ParentRoute, Route, Router, Routes, Outlet, A};
use leptos_router::path;
use leptos_router::hooks::use_params_map;

mod about;
mod practice;
mod acidinput;
mod images;

use about::*;
use practice::*;
use acidinput::*;
use images::*;


#[component]
pub fn App() -> impl IntoView {
    view! {
        <Router>
            <nav>
                <a class="nav" href="/">"产品录入"</a>
                <a class="nav" href="/practice">"练习1"</a>
                <a class="nav" href="/about">"练习2"</a>
                <a class="nav" href="/images">"图形练习"</a>
                <a class="nav" href="/embeddedpage">"嵌入HTML"</a>
                <a class="nav" href="/contacts">"联系我们"</a>
                <a class="nav" href="/embeddedweb">"嵌入网页"</a>
            </nav>
            <main>
                <Routes fallback=|| "Not found.">
                    // / just has an un-nested "Home"
                    <Route path=path!("/") view=|| view! {<AcidInput />} />
                    <Route path=path!("/practice") view=|| view! {<Practice initial_value = 8 />}/>
                    <Route path=path!("/about") view=|| view! {<About />}/>
                    <Route path=path!("/images") view=|| view! {<ImagesPage />}/>
                    <Route path=path!("/embeddedpage") view=EmbeddedPage/>
                    //<Route path=path!("/embeddedweb") view=EmbeddedWeb/>
                    <ParentRoute path=path!("/contacts") view=ContactList>
                        // if no id specified, fall back
                        <ParentRoute path=path!(":id") view=ContactInfo>
                            <Route path=path!("") view=|| view! {
                                <div class="tab">
                                    "(Contact Info)"
                                </div>
                            }/>
                            <Route path=path!("conversations") view=|| view! {
                                <div class="tab">
                                    "(Conversations)"
                                </div>
                            }/>
                        </ParentRoute>
                        // if no id specified, fall back
                        <Route path=path!("") view=|| view! {
                            <div class="select-user">
                                "Select a user to view contact info."
                            </div>
                        }/>
                    </ParentRoute>
                </Routes>                
            </main>
        </Router>
    }
}

#[component]
fn EmbeddedPage() -> impl IntoView {
    view! {
        <h1>"嵌入HTML文件"</h1>
        <iframe
            src="public/about/insert.html"
            width="100%"
            height="500px"
            style="border:none;"
        ></iframe>
    }
}

#[component]
fn EmbeddedWeb() -> impl IntoView {
    view! {
        <h1>"嵌入网站"</h1>
        <iframe
            src="https://sina.com.cn"
            width="100%"
            height="600px"
            style="border:none;"
        ></iframe>
    }
}

#[component]
fn ContactList() -> impl IntoView {
    view! {
        <div class="contact-list">
            // here's our contact list component itself
            <h3>"Contacts"</h3>
            <div class="contact-list-contacts">
                <A href="alice">"Alice"</A>
                <A href="bob">"Bob"</A>
                <A href="steve">"Steve"</A>
            </div>

            // <Outlet/> will show the nested child route
            // we can position this outlet wherever we want
            // within the layout
            <Outlet/>
        </div>
    }
}

#[component]
fn ContactInfo() -> impl IntoView {
    // we can access the :id param reactively with `use_params_map`
    let params = use_params_map();
    let id = move || params.read().get("id").unwrap_or_default();

    // imagine we're loading data from an API here
    let name = move || match id().as_str() {
        "alice" => "Alice",
        "bob" => "Bob",
        "steve" => "Steve",
        _ => "User not found.",
    };

    view! {
        <h4>{name}</h4>
        <div class="contact-info">
            <div class="tabs">
                <A href="" exact=true>"Contact Info"</A>
                <A href="conversations">"Conversations"</A>
            </div>

            // <Outlet/> here is the tabs that are nested
            // underneath the /contacts/:id route
            <Outlet/>
        </div>
    }
}

页面效果如下:

 

其中/images路径的内容主要通过app/images.rs来实现。

3. app/images.rs文件
use leptos::prelude::*;
use leptos::task::spawn_local;
use leptos::ev::SubmitEvent;
//use serde::{Deserialize, Serialize};
//use leptos::ev::Event;
use wasm_bindgen::prelude::*;
//use chrono::{Local, NaiveDateTime}; 
use leptos::web_sys::{Blob, Url};
use web_sys::BlobPropertyBag; 
use js_sys::{Array, Uint8Array};
use base64::engine::general_purpose::STANDARD; // 引入 STANDARD Engine
use base64::Engine; // 引入 Engine trait
use leptos::logging::log;

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"], js_name = invoke)]
    async fn invoke_without_args(cmd: &str) -> JsValue;

    #[wasm_bindgen(js_namespace = ["window", "__TAURI__", "core"])] //Tauri API 将会存储在 window.__TAURI__ 变量中,并通过 wasm-bindgen 导入。
    async fn invoke(cmd: &str, args: JsValue) -> JsValue;
}




#[component]
pub fn ImagesPage() -> impl IntoView {         //函数返回IntoView类型,即返回view!宏,函数名App()也是主程序view!宏中的组件名(component name)。
    let (img_error, set_img_error) = signal(String::new());
    let (video_src, set_video_src) = signal(String::new());
    
    let plot_svg_image = move|ev:SubmitEvent| {
        ev.prevent_default();
        spawn_local(async move {
            // 调用 Tauri 的 invoke 方法获取 base64 图片数据
            let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_svg_curve").await).unwrap();
            //log!("Received Base64 data: {}", result);
            let mut image = String::new();
            if result.len() != 0 {
                // 将 base64 数据存储到信号中
                image = result;
            } else {
                set_img_error.set("Failed to generate plot".to_string());
            }

            // 检查 Base64 数据是否包含前缀
            let base64_data = if image.starts_with("data:image/svg+xml;base64,") {
                image.trim_start_matches("data:image/svg+xml;base64,").to_string()
            } else {
                image
            };
            // 将 Base64 字符串解码为二进制数据
            let binary_data =  STANDARD.decode(&base64_data).expect("Failed to decode Base64");
             // 将二进制数据转换为 js_sys::Uint8Array
             let uint8_array = Uint8Array::from(&binary_data[..]);
            // 创建 Blob
            let options = BlobPropertyBag::new();
            options.set_type("image/svg+xml");
            let blob = Blob::new_with_u8_array_sequence_and_options(
                &Array::of1(&uint8_array),
                &options,
            )
            .expect("Failed to create Blob");

            // 生成图片 URL
            let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");

            // 打印生成的 URL,用于调试
            //log!("Generated Blob URL: {}", image_url);

            // 动态创建 <img> 元素
            let img = document().create_element("img").expect("Failed to create img element");
            img.set_attribute("src", &image_url).expect("Failed to set src");
            img.set_attribute("alt", "Plot").expect("Failed to set alt");
            // 设置宽度(例如 300px),高度会自动缩放
            img.set_attribute("width", "600").expect("Failed to set width");

            // 将 <img> 插入到 DOM 中
            let img_div = document().get_element_by_id("img_svg").expect("img_div not found");
            // 清空 div 内容(避免重复插入)
            img_div.set_inner_html("");
            img_div.append_child(&img).expect("Failed to append img");
        
        });
    };

    let python_plot = move|ev:SubmitEvent| {
        ev.prevent_default();
        spawn_local(async move {
            // 调用 Tauri 的 invoke 方法获取 base64 图片数据
            let result:String = serde_wasm_bindgen::from_value(invoke_without_args("python_plot").await).unwrap();
            //log!("Received Base64 data: {}", result);
            let mut image = String::new();
            if result.len() != 0 {
                // 将 base64 数据存储到信号中
                image = result;
            } else {
                set_img_error.set("Failed to generate plot".to_string());
            }

            // 检查 Base64 数据是否包含前缀data:image/png;base64
            let base64_data = if image.starts_with("data:image/png;base64,") {
                image.trim_start_matches("data:image/png;base64,").to_string()
            } else {
                image
            };

            
            // 去除多余的换行符和空格
            let base64_data = base64_data.trim().to_string();
            
            // 将 Base64 字符串解码为二进制数据
            let binary_data =  STANDARD.decode(&base64_data).expect("Failed to decode Base64");
             // 将二进制数据转换为 js_sys::Uint8Array
             let uint8_array = Uint8Array::from(&binary_data[..]);
            // 创建 Blob
            let options = BlobPropertyBag::new();
            options.set_type("image/png");
            let blob = Blob::new_with_u8_array_sequence_and_options(
                &Array::of1(&uint8_array),
                &options,
            )
            .expect("Failed to create Blob");

            // 生成图片 URL
            let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");

            // 打印生成的 URL,用于调试
            log!("Generated Blob URL: {}", image_url);

            // 动态创建 <img> 元素
            let img = document().create_element("img").expect("Failed to create img element");
            img.set_attribute("src", &image_url).expect("Failed to set src");
            img.set_attribute("alt", "Plot").expect("Failed to set alt");
            // 设置宽度(例如 300px),高度会自动缩放
            img.set_attribute("width", "600").expect("Failed to set width");

            // 将 <img> 插入到 DOM 中
            let img_div = document().get_element_by_id("img_python").expect("img_div not found");
            // 清空 div 内容(避免重复插入)
            img_div.set_inner_html("");
            img_div.append_child(&img).expect("Failed to append img");
        
        });
    };

    let plot_gif_image = move|ev:SubmitEvent| {
        ev.prevent_default();
        spawn_local(async move {
           // 调用 Tauri 的 invoke 方法获取 base64 图片数据
           let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_gif_curve").await).unwrap();
           //log!("Received Base64 data: {}", result);
           let mut image = String::new();
           if result.len() != 0 {
               // 将 base64 数据存储到信号中
               image = result;
           } else {
               set_img_error.set("Failed to generate plot".to_string());
           }

           // 检查 Base64 数据是否包含前缀
           let base64_data = if image.starts_with("data:image/gif;base64,") {
               image.trim_start_matches("data:image/gif;base64,").to_string()
           } else {
               image
           };
           // 将 Base64 字符串解码为二进制数据
           let binary_data =  STANDARD.decode(&base64_data).expect("Failed to decode Base64");
            // 将二进制数据转换为 js_sys::Uint8Array
            let uint8_array = Uint8Array::from(&binary_data[..]);
           // 创建 Blob
           let options = BlobPropertyBag::new();
           options.set_type("image/gif");
           let blob = Blob::new_with_u8_array_sequence_and_options(
               &Array::of1(&uint8_array),
               &options,
           )
           .expect("Failed to create Blob");

           // 生成图片 URL
           let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");

           // 打印生成的 URL,用于调试
           //log!("Generated Blob URL: {}", image_url);

           // 动态创建 <img> 元素
           let img = document().create_element("img").expect("Failed to create img element");
           img.set_attribute("src", &image_url).expect("Failed to set src");
           img.set_attribute("alt", "Plot").expect("Failed to set alt");
           // 设置宽度(例如 300px),高度会自动缩放
           img.set_attribute("width", "600").expect("Failed to set width");

           // 将 <img> 插入到 DOM 中
           let img_div = document().get_element_by_id("gif_div").expect("gif_div not found");
           // 清空 div 内容(避免重复插入)
           img_div.set_inner_html("");
           img_div.append_child(&img).expect("Failed to append img");
        
        });
    };


    let plot_3d_image = move|ev:SubmitEvent| {
        ev.prevent_default();
        spawn_local(async move {
            // 调用 Tauri 的 invoke 方法获取 base64 图片数据
            let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_3d_surface").await).unwrap();
            //log!("Received Base64 data: {}", result);
            let mut image = String::new();
            if result.len() != 0 {
                // 将 base64 数据存储到信号中
                image = result;
            } else {
                set_img_error.set("Failed to generate plot".to_string());
            }

            // 检查 Base64 数据是否包含前缀
            let base64_data = if image.starts_with("data:image/svg+xml;base64,") {
                image.trim_start_matches("data:image/svg+xml;base64,").to_string()
            } else {
                image
            };
            // 将 Base64 字符串解码为二进制数据
            let binary_data =  STANDARD.decode(&base64_data).expect("Failed to decode Base64");
             // 将二进制数据转换为 js_sys::Uint8Array
             let uint8_array = Uint8Array::from(&binary_data[..]);
            // 创建 Blob
            let options = BlobPropertyBag::new();
            options.set_type("image/svg+xml");
            let blob = Blob::new_with_u8_array_sequence_and_options(
                &Array::of1(&uint8_array),
                &options,
            )
            .expect("Failed to create Blob");

            // 生成图片 URL
            let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");

            // 打印生成的 URL,用于调试
            //log!("Generated Blob URL: {}", image_url);

            // 动态创建 <img> 元素
            let img = document().create_element("img").expect("Failed to create img element");
            img.set_attribute("src", &image_url).expect("Failed to set src");
            img.set_attribute("alt", "Plot").expect("Failed to set alt");
            // 设置宽度(例如 300px),高度会自动缩放
            img.set_attribute("width", "600").expect("Failed to set width");

            // 将 <img> 插入到 DOM 中
            let img_div = document().get_element_by_id("3d_div").expect("img_3d not found");
            // 清空 div 内容(避免重复插入)
            img_div.set_inner_html("");
            img_div.append_child(&img).expect("Failed to append img");
        
        });
    };


    let plot_gif_3d = move|ev:SubmitEvent| {
        ev.prevent_default();
        spawn_local(async move {
           // 调用 Tauri 的 invoke 方法获取 base64 图片数据
           let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_gif_3d").await).unwrap();
           //log!("Received Base64 data: {}", result);
           let mut image = String::new();
           if result.len() != 0 {
               // 将 base64 数据存储到信号中
               image = result;
           } else {
               set_img_error.set("Failed to generate plot".to_string());
           }

           // 检查 Base64 数据是否包含前缀
           let base64_data = if image.starts_with("data:image/gif;base64,") {
               image.trim_start_matches("data:image/gif;base64,").to_string()
           } else {
               image
           };
           // 将 Base64 字符串解码为二进制数据
           let binary_data =  STANDARD.decode(&base64_data).expect("Failed to decode Base64");
            // 将二进制数据转换为 js_sys::Uint8Array
            let uint8_array = Uint8Array::from(&binary_data[..]);
           // 创建 Blob
           let options = BlobPropertyBag::new();
           options.set_type("image/gif");
           let blob = Blob::new_with_u8_array_sequence_and_options(
               &Array::of1(&uint8_array),
               &options,
           )
           .expect("Failed to create Blob");

           // 生成图片 URL
           let image_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");

           // 打印生成的 URL,用于调试
           //log!("Generated Blob URL: {}", image_url);

           // 动态创建 <img> 元素
           let img = document().create_element("img").expect("Failed to create img element");
           img.set_attribute("src", &image_url).expect("Failed to set src");
           img.set_attribute("alt", "Plot").expect("Failed to set alt");
           // 设置宽度(例如 300px),高度会自动缩放
           img.set_attribute("width", "600").expect("Failed to set width");

           // 将 <img> 插入到 DOM 中
           let img_div = document().get_element_by_id("gif_3d").expect("gif_3d not found");
           // 清空 div 内容(避免重复插入)
           img_div.set_inner_html("");
           img_div.append_child(&img).expect("Failed to append img");
        
        });
    };


    let plot_mp4_3d = move|ev:SubmitEvent| {
        ev.prevent_default();
        spawn_local(async move {
           // 调用 Tauri 的 invoke 方法获取 base64 图片数据
           let result:String = serde_wasm_bindgen::from_value(invoke_without_args("generate_mp4_3d").await).unwrap();
           //log!("Received Base64 data: {}", result);
           let mut image = String::new();
           if result.len() != 0 {
               // 将 base64 数据存储到信号中
               image = result;
           } else {
               set_img_error.set("Failed to generate plot".to_string());
           }

           // 检查 Base64 数据是否包含前缀
           let base64_data = if image.starts_with("data:video/mp4;base64,") {
               image.trim_start_matches("data:video/mp4;base64,").to_string()
           } else {
               image
           };
           // 将 Base64 字符串解码为二进制数据
           let binary_data =  STANDARD.decode(&base64_data).expect("Failed to decode Base64");
            // 将二进制数据转换为 js_sys::Uint8Array
            let uint8_array = Uint8Array::from(&binary_data[..]);
           // 创建 Blob
           let options = BlobPropertyBag::new();
           options.set_type("video/mp4");
           let blob = Blob::new_with_u8_array_sequence_and_options(
               &Array::of1(&uint8_array),
               &options,
           )
           .expect("Failed to create Blob");

           // 生成视频 URL
           let video_url = Url::create_object_url_with_blob(&blob).expect("Failed to create URL");

           // 打印生成的 URL,用于调试
           //log!("Generated Blob URL: {}", video_url);

           set_video_src.set(video_url);          
        
        });
    };




    view! {
        <main class="container">
            <h1>"Images Page"</h1>
            <div>
                <p class="red">{move || img_error.get() }</p>
                <p>"Generate PNG Image"</p>
                <div id="img_div">
                <img
                src=""
                width="600"
                />
                </div>
                <form id="form_svg" on:submit=plot_svg_image>
                    <button type="submit">"Generate SVG Image"</button>
                    <p></p>
                    <div id="img_svg">
                    <img
                    src=""
                    width="600"
                    />
                    </div>
                </form>
                <form id="form_python" on:submit=python_plot>
                    <button type="submit">"Generate By Python"</button>
                    <p></p>
                    <div id="img_python">
                    <img
                    src=""
                    width="600"
                    />
                    </div>
                </form>
                <form id="form_gif" on:submit=plot_gif_image>
                    <button type="submit">"Generate GIF"</button>
                    <p></p>
                    <div id="gif_div">
                    <img
                    src=""
                    width="600"
                    />
                    </div>
                </form>
                <form id="form_3d" on:submit=plot_3d_image>
                    <button type="submit">"Generate 3D Surface"</button>
                    <p></p>
                    <div id="3d_div">
                    <img
                    src=""
                    width="600"
                    />
                    </div>
                </form>
                <form id="form_gif_3d" on:submit=plot_gif_3d>
                    <button type="submit">"Generate 3D GIF"</button>
                    <p></p>
                    <div id="gif_3d">
                    <img
                    src=""
                    width="600"
                    />
                    </div>
                </form>
                <form on:submit=plot_mp4_3d>
                    <button type="submit">"Generate Video"</button>
                    <div id="mp4_3d">
                        <video
                            src=video_src
                            controls=true
                            width="600"
                            style="margin-top: 20px;"
                        >
                            Your browser does not support the video tag.
                        </video>
                    </div>
                </form>
            </div>

        </main>
    }
}

该文件中定义了所有绘图功能的显示,其中生成png图片是由另外页面按钮调用切换到该页面后显示的,页面效果如下:

 4. 绘图效果
(1)绘制SVG格式图片

(2)调用python的Matplotlib绘制图形

python代码被放在src-tauri\resources目录下,文件名plot.py,内容如下:

# -*_ coding: utf-8 -*-
 
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.ticker import LinearLocator, FormatStrFormatter
import matplotlib as mpl
import base64
from io import BytesIO

mpl.rcParams.update({
    "font.family": "sans-serif",
    "font.sans-serif": "SimHei", 
    "axes.unicode_minus":False
    })

 
n = 101
R = 12
r = 5
 
u, v=np.meshgrid(np.linspace(0, 2*np.pi, n), np.linspace(0, 2*np.pi, n))
 
x = (R - r*np.sin(v))*np.sin(u)
y = (R - r*np.sin(v))*np.cos(u)
z = r*np.cos(v)
 
 
 
fig = plt.figure(figsize=(13, 10))
ax3d = plt.axes(projection='3d')
 
#设置画布透明
fig.patch.set_alpha(0)
#设置绘图区域透明
ax3d.patch.set_alpha(0.0)
#设置边框透明
for spine in ax3d.spines.values():
    spine.set_alpha(0)
 
ax3d.set_xlabel("X")
ax3d.set_ylabel("Y")
ax3d.set_zlabel("Z")
ax3d.set_zlim(-15, 15)
ax3d.set_xlim(-15, 15)
ax3d.set_ylim(-15, 15)
ax3d.zaxis.set_major_locator(LinearLocator(10))
ax3d.zaxis.set_major_formatter(FormatStrFormatter('%.01f'))
 
plt.tick_params(labelsize=10)
 
surf = ax3d.plot_surface(x, y, z, cmap=plt.cm.Spectral)
 
 
ax3d.set_title("四次元百宝袋", fontsize=96, bbox={'facecolor': 'gold', 'alpha':0.8,  'pad': 10})
 

ax3d.view_init(30, 45)

plt.tight_layout()

# 将图像保存为 Base64
buf = BytesIO()
plt.savefig(buf, dpi=400, format='png', transparent=True, facecolor = 'none')
buf.seek(0)
image_base64 = base64.b64encode(buf.read()).decode('utf-8')
# 打印 Base64 字符串(确保没有换行符)
print(f"data:image/png;base64,{image_base64}", end="")

 为了便于程序编译定位到plot.py文件,需要修改tauri.conf.json文件的"beforeBuildCommand",添加"resources":。

  "build": {
    "beforeDevCommand": "trunk serve",
    "devUrl": "http://localhost:1420",
    "beforeBuildCommand": "trunk build && xcopy /E /I src-tauri\\resources src-tauri\\target\\release\\resources",
    "frontendDist": "../dist"
  },


  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png",
      "icons/icon.icns",
      "icons/icon.ico"
    ],
    "resources":["resources/plot.py"]
  }
(3)绘制GIF动画

 

(4)绘制SVG格式的三维立体图形

 然后制作了响应的GIF动画(文件太大,不能上传)

(5)制作mp4视频

mp4视频是通过Plotters结合video_rs来完成的,Windows下需要预装LLVM和FFMpeg。视频效果如下: 

Plotters结合video_rs生成mp4视频

 

二、Tauri后台绘图命令

lib.rs文件内容如下,里面包括一些前期工作写的命令,供前端Leptos调用。

use full_palette::PURPLE;
use futures::TryStreamExt;
use plotters::prelude::*;
use std::path::Path;
use sqlx::{migrate::MigrateDatabase, prelude::FromRow, sqlite::SqlitePoolOptions, Pool, Sqlite};
//use tauri::{App, Manager, WebviewWindowBuilder, Emitter};
use tauri::{menu::{CheckMenuItem, Menu, MenuItem, Submenu}, App, Emitter, Listener, Manager, WebviewWindowBuilder};
use serde::{Deserialize, Serialize};
type Db = Pool<Sqlite>;
use image::{ExtendedColorType, ImageBuffer, ImageEncoder, Rgba, DynamicImage, RgbImage};
use image::codecs::png::PngEncoder; // 引入 PngEncoder
use std::process::Command;
use std::env;

struct DbState {
    db: Db,
}



async fn setup_db(app: &App) -> Db {
    let mut path = app.path().app_data_dir().expect("获取程序数据文件夹路径失败!");
 
    match std::fs::create_dir_all(path.clone()) {
        Ok(_) => {}
        Err(err) => {
            panic!("创建文件夹错误:{}", err);
        }
    };

    //C:\Users\<user_name>\AppData\Roaming\com.mynewapp.app\db.sqlite 
    path.push("db.sqlite");
 
    Sqlite::create_database(
        format!("sqlite:{}", path.to_str().expect("文件夹路径不能为空!")).as_str(),
        )
        .await
        .expect("创建数据库失败!");
 
    let db = SqlitePoolOptions::new()
        .connect(path.to_str().unwrap())
        .await
        .unwrap();
    
    //创建迁移文件位于./migrations/文件夹下    
    //cd src-tauri
    //sqlx migrate add create_users_table
    sqlx::migrate!("./migrations/").run(&db).await.unwrap();
 
    db
}


// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
#[tauri::command]
fn greet(name: &str) -> String {
    format!("你好, {}!Rust向你问候了!", name)
}


#[derive(Debug, Serialize, Deserialize, FromRow)]
struct User {
    id: u16,
    username: String,
    email: String,
}

#[derive(Debug, Serialize, Deserialize, FromRow)]
struct UserId {
    id: u16,
}

#[derive(Debug, Serialize, Deserialize, FromRow)]
struct ProductId {
    pdt_id: i64,
}


#[derive(Serialize, Deserialize)]
struct Product {
    pdt_name:String,
    pdt_si:f64,
    pdt_al:f64,
    pdt_ca:f64,
    pdt_mg:f64,
    pdt_fe:f64,
    pdt_ti:f64,
    pdt_ka:f64,
    pdt_na:f64,
    pdt_mn:f64,
    pdt_date:String,
}

#[derive(Debug, Serialize, Deserialize, FromRow)]
struct Pdt {
    pdt_id:i64,         //sqlx 会将 SQLite 的 INTEGER 类型映射为 i64(64 位有符号整数)
    pdt_name:String,
    pdt_si:f64,
    pdt_al:f64,
    pdt_ca:f64,
    pdt_mg:f64,
    pdt_fe:f64,
    pdt_ti:f64,
    pdt_ka:f64,
    pdt_na:f64,
    pdt_mn:f64,
    pdt_date:String,
}


#[tauri::command]
async fn get_db_value(state: tauri::State<'_, DbState>, window: tauri::Window) -> Result<String, String> {
    let db = &state.db;
    let query_result:Vec<User> = sqlx::query_as::<_, User>(       //查询数据以特定的格式输出
        "SELECT * FROM users"
        )
        .fetch(db)
        .try_collect()
        .await.unwrap();
    
    let mut div_content = String::new();
    for user in query_result.iter(){
        div_content += &format!(r###"<p><input type="checkbox" name="items" value="{}"> ID:{},姓名:{},邮箱:{}</p>"###, user.id, user.id, user.username, user.email);
    }   
    // 获取当前窗口
    let current_window = window.get_webview_window("main").unwrap();
    let script = &format!("document.getElementById('db-item').innerHTML = '{}';",div_content);
    current_window.eval(script).unwrap();

    Ok(String::from("数据库读取成功!"))
}

#[tauri::command]
async fn send_db_item(state: tauri::State<'_, DbState>) -> Result<Vec<User>, String> {
    let db = &state.db;
    let query_result:Vec<User> = sqlx::query_as::<_, User>(       //查询数据以特定的格式输出
        "SELECT * FROM users"
        )
        .fetch(db)
        .try_collect()
        .await.unwrap();
    Ok(query_result)
}

#[tauri::command]
async fn send_pdt_db(state: tauri::State<'_, DbState>) -> Result<Vec<Pdt>, String> {
    let db = &state.db;
    let query_result:Vec<Pdt> = sqlx::query_as::<_, Pdt>(       //查询数据以特定的格式输出
        "SELECT * FROM products"
        )
        .fetch(db)
        .try_collect()
        .await.unwrap();
    Ok(query_result)
}

#[tauri::command]
async fn write_pdt_db(state: tauri::State<'_, DbState>, product:Product) -> Result<String, String> {
    let db = &state.db;

    sqlx::query("INSERT INTO products (pdt_name, pdt_si, pdt_al, pdt_ca, pdt_mg, pdt_fe, pdt_ti, pdt_ka, pdt_na, pdt_mn, pdt_date) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?11)")
        .bind(product.pdt_name)
        .bind(product.pdt_si)
        .bind(product.pdt_al)
        .bind(product.pdt_ca)
        .bind(product.pdt_mg)
        .bind(product.pdt_fe)
        .bind(product.pdt_ti)
        .bind(product.pdt_ka)
        .bind(product.pdt_na)
        .bind(product.pdt_mn)
        .bind(product.pdt_date)
        .execute(db)
        .await
        .map_err(|e| format!("数据库插入项目错误: {}", e))?;
    
    Ok(String::from("插入数据成功!"))
}


//use tauri::Error;
#[tauri::command]
async fn insert_db_item(state: tauri::State<'_, DbState>, username: &str, email: Option<&str>) -> Result<String, String> {
    let db = &state.db;
    email.unwrap_or("not set yet");     //email类型为Option<&str>,其结果为None或者Some(&str)

    sqlx::query("INSERT INTO users (username, email) VALUES (?1, ?2)")
        .bind(username)
        .bind(email)
        .execute(db)
        .await
        .map_err(|e| format!("数据库插入项目错误: {}", e))?;
    
    Ok(String::from("插入数据成功!"))
}

#[tauri::command]
async fn update_user(state: tauri::State<'_, DbState>, user: User) -> Result<(), String> {
    let db = &state.db;

    sqlx::query("UPDATE users SET username = ?1, email = ?2 WHERE id = ?3")
        .bind(user.username)
        .bind(user.email)
        .bind(user.id)
        .execute(db)
        .await
        .map_err(|e| format!("不能更新user:{}", e))?;

    Ok(())
}


#[tauri::command]
async fn del_last_user(state: tauri::State<'_, DbState>) -> Result<String, String> {
    let db = &state.db;

    let last_id:UserId = sqlx::query_as::<_,UserId>("SELECT id FROM users ORDER BY id DESC LIMIT 1")
        .fetch_one(db)
        .await
        .unwrap();

        sqlx::query("DELETE FROM users WHERE id = ?1")
        .bind(last_id.id)
        .execute(db)
        .await
        .map_err(|e| format!("could not delete last user: {}", e))?;

        Ok(String::from("最后一条数据删除成功!"))
}

#[tauri::command]
async fn del_last_pdt(state: tauri::State<'_, DbState>) -> Result<String, String> {
    let db = &state.db;

    let last_id:ProductId = sqlx::query_as::<_,ProductId>("SELECT pdt_id FROM products ORDER BY pdt_id DESC LIMIT 1")
        .fetch_one(db)
        .await
        .unwrap();

        sqlx::query("DELETE FROM products WHERE pdt_id = ?1")
        .bind(last_id.pdt_id)
        .execute(db)
        .await
        .map_err(|e| format!("could not delete last product: {}", e))?;

        Ok(String::from("最后一条数据删除成功!"))
}

#[tauri::command]
async fn open_new_window(app: tauri::AppHandle, title:String, url:String) -> Result<(), String>{
    let main_window = app.get_webview_window("main").unwrap();
    let toggle = MenuItem::with_id(&app, "quit", "退出", true, None::<&str>).unwrap();

    let submenu_1 = CheckMenuItem::with_id(&app, "check_me", "显示窗口",true, true, None::<&str>).unwrap();
    let submenu_2 =CheckMenuItem::with_id(&app, "check_you", "隐藏窗口",true, false, None::<&str>).unwrap();
    let check_menu = Submenu::with_id_and_items(&app, "check_one", "选择", true, &[&submenu_1, &submenu_2]).unwrap();

    let  menu = Menu::with_items(&app, &[&toggle, &check_menu]).unwrap();

    let window = WebviewWindowBuilder::new(
        &app, 
        "about", 
        tauri::WebviewUrl::App(url.into()))
        .parent(&main_window).unwrap()
        .always_on_top(false)
        .title(&title)
        .inner_size(800.0, 600.0)
        .center()
        .menu(menu)
        .build()
        .unwrap();

    window.on_menu_event(move|app, event| {
        match event.id.as_ref() {
            "quit" => {
                let _ = app.close();
            },
            "check_you" => {
                let _ = app.hide();
            },
            "check_me" => {
                let _ = app.show();
            },
            _ => {}
        }
    });

    // 动态注入内容
    //window
    //    .eval("document.getElementById('insert_div').innerHTML = '</br><h1>Hello from dynamically injected content!</h1>';")
    //    .expect("Failed to inject content");

    window.show().unwrap();
    // 监听主窗口发送的事件
    window.clone().listen("update-content", move |event| {
        let script = &format!("document.getElementById('insert_div').innerHTML = '{}';",event.payload());
        window.eval(script).unwrap();
        });
        
    Ok(())
}

#[tauri::command]
async fn close_window(app: tauri::AppHandle) -> Result<(), String>{
    if let Some(window) = app.get_webview_window("about"){
        window.close().unwrap();
    }
    Ok(())
}

#[tauri::command]
async fn show_window(app: tauri::AppHandle) -> Result<(), String>{
    if let Some(window) = app.get_webview_window("about"){
        window.show().unwrap();
    }
    Ok(())
}

#[tauri::command]
async fn close_main_window(app: tauri::AppHandle) -> Result<(), String>{
    if let Some(window) = app.get_webview_window("main"){
        window.close().unwrap();
    }
    Ok(())
}


#[tauri::command]
async fn insert_div(app: tauri::AppHandle, label:String, content:String) -> Result<(), String>{
    if let Some(window) = app.get_webview_window(&label){
        window
            .eval(&format!("document.getElementById('insert_div').innerHTML = '{}';",content))
            .expect("Failed to inject content");
    }
    Ok(())
}

//从主窗口向其它窗口发送event
#[tauri::command]
async fn emit_event(app: tauri::AppHandle, label:String, content:String) -> Result<(), String>{
    if let Some(window) = app.get_webview_window(&label){
        window
        .emit_to(&label, "update-content", content)     //.emit_to(target, event, content)
        .unwrap();
    }
    Ok(())
}


use base64::engine::general_purpose::STANDARD;
use base64::Engine;


// 生成图表并返回 Base64 编码的 PNG 图片
#[tauri::command]
async fn generate_plot() -> Result<String, String> {
    // 创建一个缓冲区,大小为 800x600 的 RGBA 图像
    let mut buffer = vec![0; 800 * 600 * 3]; // 800x600 图像,每个像素 3 字节(RGB)

    {
        // 使用缓冲区创建 BitMapBackend
        let root = BitMapBackend::with_buffer(&mut buffer, (800, 600)).into_drawing_area();
        root.fill(&WHITE).map_err(|e| e.to_string())?;

        // 定义绘图区域
        let mut chart = ChartBuilder::on(&root)
            .caption("Sine Curve", ("sans-serif", 50).into_font())
            .build_cartesian_2d(-10.0..10.0, -1.5..1.5) // X 轴范围:-10 到 10,Y 轴范围:-1.5 到 1.5
            .map_err(|e| e.to_string())?;

        // 绘制正弦曲线
        chart
            .draw_series(LineSeries::new(
                (-100..=100).map(|x| {
                    let x_val = x as f64 * 0.1; // 将 x 转换为浮点数
                    (x_val, x_val.sin()) // 计算正弦值
                }),
                &RED, // 使用红色绘制曲线
            ))
            .map_err(|e| e.to_string())?;

        // 将图表写入缓冲区
        root.present().map_err(|e| e.to_string())?;
    } // 这里 `root` 离开作用域,释放对 `buffer` 的可变借用

    // 将 RGB 数据转换为 RGBA 数据(添加 Alpha 通道)
    let mut rgba_buffer = Vec::with_capacity(800 * 600 * 4);
    for pixel in buffer.chunks(3) {
        // 判断是否为背景色(RGB 值为 (255, 255, 255))
        let is_background = pixel[0] == 255 && pixel[1] == 255 && pixel[2] == 255;

        // 设置 Alpha 通道的值
        let alpha = if is_background {
            0 // 背景部分完全透明
        } else {
            255 // 其他部分完全不透明
        };

        rgba_buffer.extend_from_slice(&[pixel[0], pixel[1], pixel[2], alpha]); // 添加 Alpha 通道
    }

    // 将缓冲区的 RGBA 数据转换为 PNG 格式
    let image_buffer: ImageBuffer<Rgba<u8>, _> =
    ImageBuffer::from_raw(800, 600, rgba_buffer).ok_or("Failed to create image buffer")?;

    // 直接保存图片,检查是否乱码
    //image_buffer.save("output.png").map_err(|e| e.to_string())?;
    
    // 将 PNG 数据编码为 Base64
    let mut png_data = Vec::new();
    let encoder = PngEncoder::new(&mut png_data);
    encoder
        .write_image(
            &image_buffer.to_vec(),
            800,
            600,
            ExtendedColorType::Rgba8,
        )
        .map_err(|e| e.to_string())?;

    // 将图片数据转换为 Base64 编码的字符串
    let base64_data = STANDARD.encode(&png_data);
    //use std::fs::File;
    //use std::io::Write;
    // 创建或打开文件
    //let file_path = "output.txt"; // 输出文件路径
    //let mut file = File::create(file_path).unwrap();

    // 将 base64_data 写入文件
    //file.write_all(base64_data.as_bytes()).unwrap();

    // 返回 Base64 编码的图片数据
    Ok(format!("data:image/png;base64,{}", base64_data))
}

// 绘制正弦曲线并返回 Base64 编码的 SVG 数据
#[tauri::command]
fn generate_svg_curve() -> Result<String, String> {
    // 创建一个字符串缓冲区,用于存储 SVG 数据
    let mut buffer = String::new();

    {
        // 使用缓冲区创建 SVGBackend
        let root = SVGBackend::with_string(&mut buffer, (800, 800)).into_drawing_area();

        // 设置背景为透明
        root.fill(&TRANSPARENT).map_err(|e| e.to_string()).unwrap();

        // 创建字体描述,设置粗体和斜体
        //let font = FontDesc::new(FontFamily::Name("微软雅黑"), 50.0, FontStyle::Normal).style(FontStyle::Bold);
        let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体

        // 将 FontDesc 转换为 TextStyle
        let yhfont = font
            .into_text_style(&root)        //.transform(FontTransform::Rotate180)
            .color(&RED);

        // 定义绘图区域
        let mut chart = ChartBuilder::on(&root)
        .caption("一元二次方程", yhfont)        //.caption("正弦曲线", ("微软雅黑", 50).into_font())
        .margin(10)
        .x_label_area_size(50)
        .y_label_area_size(50)
        .build_cartesian_2d(-4f32..4f32, 0f32..20f32) // X 轴范围:-10 到 10,Y 轴范围:-1到 10
        .map_err(|e| e.to_string())?;


        //chart.configure_mesh().draw().map_err(|e| e.to_string())?;
        // 设置标签的字体大小
        chart
        .configure_mesh()
        .x_labels(5)   //X 轴主要标签数量
        .y_labels(5)
        .x_label_style(("sans-serif", 20).into_font())
        .y_label_style(("sans-serif", 20).into_font())
        .x_label_formatter(&|x| format!("{:.1}", x)) // 格式化 X 轴标签
        .y_label_formatter(&|y| format!("{:.1}", y)) // 格式化 Y 轴标签
        //.disable_y_mesh() // 隐藏 Y 轴网格线,.disable_x_mesh() // 隐藏 X 轴网格线
        .light_line_style(&BLACK.mix(0.1)) // 设置次要网格线样式
        .bold_line_style(&BLACK.mix(0.3)) // 设置主要网格线样式
        .axis_style(&BLACK) // 设置坐标轴本身的样式
        .draw()
        .map_err(|e| e.to_string())?;

        // 绘制二次方程
        chart
            .draw_series(DashedLineSeries::new(
                (-200..=200).map(|x| x as f32/50.0).map(|x|(x,x*x)),
                10,  /* size = length of dash */
                5, /* size = spacing of dash */
                ShapeStyle {
                    color: RED.to_rgba(),
                    filled: false,
                    stroke_width: 3,
                },
            ))
            .map_err(|e| e.to_string())?
            .label("一元二次方程")
            .legend(|(x, y)| PathElement::new(vec![(x-20, y), (x-5, y)], &RED));
            //.legend(|(x,y)| Rectangle::new([(x - 20, y + 1), (x, y)], BLACK));
            
        
        chart
            .configure_series_labels()
            .position(SeriesLabelPosition::UpperRight)
            .margin(25)
            .legend_area_size(10)
            .border_style(&BLUE)
            .background_style(&PURPLE.mix(0.8))
            .label_font(("微软雅黑", 20))
            .draw()
            .map_err(|e| e.to_string())?;

        // 将图表写入缓冲区
        root.present().map_err(|e| e.to_string())?;
    }

    // 将 SVG 数据转换为 Base64 编码的字符串
    let base64_data = STANDARD.encode(&buffer);

    // 返回 Base64 编码的 SVG 数据
    Ok(format!("data:image/svg+xml;base64,{}", base64_data))
}

fn pdf(x: f64, y: f64) -> f64 {
    const SDX: f64 = 0.1;
    const SDY: f64 = 0.1;
    const A: f64 = 5.0;
    let x = x / 10.0;
    let y = y / 10.0;
    A * (-x * x / 2.0 / SDX / SDX - y * y / 2.0 / SDY / SDY).exp()
}

// 绘制正弦曲线并返回 Base64 编码的 SVG 数据
use uuid::Uuid;
use tauri::path::BaseDirectory;
use std::fs::read;

#[tauri::command]
fn generate_gif_curve(app: tauri::AppHandle) -> Result<String, String> {
    // Generate a unique file name using UUID
    let file_name = format!("{}.gif", Uuid::new_v4());

    
    // Get the app data directory
    let app_data_dir = app
        .path()
        .resolve(&file_name, BaseDirectory::AppData)
        .expect("Failed to resolve app data directory");

    
    // Create the directory if it doesn't exist
    if let Some(parent) = app_data_dir.parent() {
        std::fs::create_dir_all(parent)
        .map_err(|e| e.to_string())?;
    }


    {
        // 使用缓冲区创建 gifBackend
        let root = BitMapBackend::gif(&app_data_dir, (600, 400), 100).unwrap().into_drawing_area();

        // for pitch in 0..157 {
        for pitch in 0..50 {
            root.fill(&WHITE)
            .map_err(|e| e.to_string())?;
    
            let mut chart = ChartBuilder::on(&root)
                .caption("2D Gaussian PDF", ("sans-serif", 20))
                .build_cartesian_3d(-3.0..3.0, 0.0..6.0, -3.0..3.0)
                .map_err(|e| e.to_string())?;

            chart.with_projection(|mut p| {
                p.pitch = 1.57 - (1.57 - pitch as f64 / 50.0).abs();
                p.scale = 0.7;
                p.into_matrix() // build the projection matrix
            });
    
            chart
                .configure_axes()
                .light_grid_style(BLACK.mix(0.15))
                .max_light_lines(3)
                .draw()
                .map_err(|e| e.to_string())?;
    
            chart.draw_series(
                SurfaceSeries::xoz(
                    (-15..=15).map(|x| x as f64 / 5.0),
                    (-15..=15).map(|x| x as f64 / 5.0),
                    pdf,
                )
                .style_func(&|&v| (VulcanoHSL::get_color(v / 5.0)).into()),
            )
            .map_err(|e| e.to_string())?;
    
            root.present()
            .map_err(|e| e.to_string())?;
        }
    

        // 将图表写入缓冲区
        root.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir");
    }

    //println!("Gif file has been saved to {:?}", app_data_dir);


    // 读取文件内容
    let file_data = read(app_data_dir).map_err(|e| e.to_string())?;

    // 将文件内容转换为 Base64
    let base64_data = STANDARD.encode(&file_data);

    // 返回 Base64 编码的图片数据
    Ok(format!("data:image/gif;base64,{}", base64_data))
}



#[tauri::command]
fn python_plot(app: tauri::AppHandle) -> Result<String, String> {
    let resource_path = app.path().resolve("resources/plot.py", BaseDirectory::Resource) // 解析资源文件路径
        .expect("Failed to resolve resource");

    // 调用 Python 脚本
    let output = Command::new("E:/python_envs/eric7/python.exe")
        .arg(resource_path) // Python 脚本路径
        .output()
        .map_err(|e| e.to_string())?;


    // 调用打包后的 Python 可执行文件
    /*
    let output = Command::new("E:/Rust_Program/tauri-app/acid-index/src-tauri/dist/plot.exe")
        .output()
        .map_err(|e| e.to_string())?;
    */

    // 检查 Python 脚本是否成功运行
    if output.status.success() {
    // 获取 Python 脚本的输出(Base64 图像数据)
    let image_data = String::from_utf8(output.stdout).map_err(|e| e.to_string())?;
    // 去除多余的换行符
    let image_data = image_data.trim().to_string();
    Ok(image_data)
    } else {
    // 获取 Python 脚本的错误输出
    let error_message = String::from_utf8(output.stderr).map_err(|e| e.to_string())?;
    Err(error_message)
    }
}

fn sin_sqrt(x: f64, y: f64) -> f64 {
    const SQT: f64 = 3.0;
    //let x = x / 10.0;
    //let y = y / 10.0;
    SQT * ((x * x + y * y ).sqrt()).sin().exp()
}


#[tauri::command]
fn generate_3d_surface() -> Result<String, String> {
    // 创建一个字符串缓冲区,用于存储 SVG 数据
    let mut buffer = String::new();

    {
        // 使用缓冲区创建 SVGBackend
        let root = SVGBackend::with_string(&mut buffer, (800, 800)).into_drawing_area();

        // 设置背景为透明
        root.fill(&TRANSPARENT).map_err(|e| e.to_string()).unwrap();

        // 创建字体描述,设置粗体和斜体
        //let font = FontDesc::new(FontFamily::Name("微软雅黑"), 50.0, FontStyle::Normal).style(FontStyle::Bold);
        let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体

        // 将 FontDesc 转换为 TextStyle
        let yhfont = font
            .into_text_style(&root)        //.transform(FontTransform::Rotate180)
            .color(&RED);

        // 定义绘图区域
        let mut chart = ChartBuilder::on(&root)
        .caption("三维曲面", yhfont)        //.caption("正弦曲线", ("微软雅黑", 50).into_font())
        .margin(10)
        .build_cartesian_3d(-6.0..6.0, -0.0..10.0, -6.0..6.0)
        .map_err(|e| e.to_string())?;

        // 通过闭包配置投影矩阵
        chart.with_projection(|mut p| {
            p.yaw = 0.5;   // 设置偏航角(绕 Y 轴旋转)
            p.pitch = 0.5; // 设置俯仰角
            p.scale = 0.75;  // 设置缩放比例
            p.into_matrix() // 返回 ProjectionMatrix
        });


        chart
            .configure_axes()
            .light_grid_style(BLACK.mix(0.15))
            .max_light_lines(3)
            .draw()
            .map_err(|e| e.to_string())?;

        chart.draw_series(
            SurfaceSeries::xoz(
                (-30..=30).map(|x| x as f64 / 5.0),
                (-30..=30).map(|x| x as f64 / 5.0),
                sin_sqrt,
            )
            .style_func(&|&v| (VulcanoHSL::get_color(v / 8.0)).into()),
        )
        .map_err(|e| e.to_string())?;

        root.present()
        .map_err(|e| e.to_string())?;
    }

    // 将 SVG 数据转换为 Base64 编码的字符串
    let base64_data = STANDARD.encode(&buffer);

    // 返回 Base64 编码的 SVG 数据
    Ok(format!("data:image/svg+xml;base64,{}", base64_data))
}


#[tauri::command]
fn generate_gif_3d(app: tauri::AppHandle) -> Result<String, String> {
    // Generate a unique file name using UUID
    let file_name = format!("{}.gif", Uuid::new_v4());

    
    // Get the app data directory
    let app_data_dir = app
        .path()
        .resolve(&file_name, BaseDirectory::AppData)
        .expect("Failed to resolve app data directory");

    
    // Create the directory if it doesn't exist
    if let Some(parent) = app_data_dir.parent() {
        std::fs::create_dir_all(parent)
        .map_err(|e| e.to_string())?;
    }


    {
        // 使用缓冲区创建 gifBackend
        let root = BitMapBackend::gif(&app_data_dir, (600, 620), 25).unwrap().into_drawing_area();
        // 创建字体描述,设置粗体和斜体
        
        //let font = FontDesc::new(FontFamily::Name("微软雅黑"), 50.0, FontStyle::Normal).style(FontStyle::Bold);
        let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体

        // 将 FontDesc 转换为 TextStyle
        let yhfont = font
            .into_text_style(&root)        //.transform(FontTransform::Rotate180)
            .color(&RED);


        for yaw in 0..180 {
            root.fill(&WHITE.mix(0.3))
            .map_err(|e| e.to_string())?;
    
        // 定义绘图区域
        let mut chart = ChartBuilder::on(&root)
        .caption("三维曲面", yhfont.clone())        //.caption("正弦曲线", ("微软雅黑", 50).into_font())
        .margin(0)
        .build_cartesian_3d(-6.0..6.0, -0.0..10.0, -6.0..6.0)
        .map_err(|e| e.to_string())?;

        // 通过闭包配置投影矩阵
        chart.with_projection(|mut p| {
            p.yaw = yaw as f64/100.00;   // 设置偏航角(绕 Y 轴旋转)
            p.pitch = 0.5; // 设置俯仰角
            p.scale = 0.75;  // 设置缩放比例
            p.into_matrix() // 返回 ProjectionMatrix
        });


        chart
            .configure_axes()
            .light_grid_style(BLACK.mix(0.15))
            .max_light_lines(3)
            .draw()
            .map_err(|e| e.to_string())?;

        chart.draw_series(
            SurfaceSeries::xoz(
                (-30..=30).map(|x| x as f64 / 5.0),
                (-30..=30).map(|x| x as f64 / 5.0),
                sin_sqrt,
            )
            .style_func(&|&v| (VulcanoHSL::get_color(v / (8.0 - (yaw as f64/4.0)%6.0).abs())).into()),
        )
        .map_err(|e| e.to_string())?;
    
            root.present()
            .map_err(|e| e.to_string())?;
        }
    

        // 将图表写入缓冲区
        root.present().expect("Unable to write result to file, please make sure 'plotters-doc-data' dir exists under current dir");
    }

    //println!("Gif file has been saved to {:?}", app_data_dir);


    // 读取文件内容
    let file_data = read(app_data_dir).map_err(|e| e.to_string())?;

    // 将文件内容转换为 Base64
    let base64_data = STANDARD.encode(&file_data);

    // 返回 Base64 编码的图片数据
    Ok(format!("data:image/gif;base64,{}", base64_data))
}


use video_rs::encode::{Encoder, Settings};
use video_rs::time::Time;
use ndarray::Array3;
#[tauri::command]
fn generate_mp4_3d(app: tauri::AppHandle) -> Result<String, String> {
    // Generate a unique file name using UUID
    let file_name = format!("{}.mp4", Uuid::new_v4());

    
    // Get the app data directory
    let app_data_dir = app
        .path()
        .resolve(&file_name, BaseDirectory::AppData)
        .expect("Failed to resolve app data directory");

    
    // Create the directory if it doesn't exist
    if let Some(parent) = app_data_dir.parent() {
        std::fs::create_dir_all(parent)
        .map_err(|e| e.to_string())?;
    }

    // Initialize video encoder
    let settings = Settings::preset_h264_yuv420p(600, 620, false); 
    let mut encoder = Encoder::new(app_data_dir.as_ref() as &Path, settings).map_err(|e| e.to_string())?;

    let frame_rate = 25; // 帧率
    let frame_duration_sec = 1.0 / frame_rate as f64; // 每帧的持续时间(秒)

    
    for yaw in 0..180 {
        // Create a buffer to hold the frame image
        let mut buffer = vec![0; 600 * 620 * 3];
        {
            let root = BitMapBackend::with_buffer(&mut buffer, (600, 620)).into_drawing_area();

            root.fill(&WHITE)
            .map_err(|e| e.to_string())?;

            let font = ("微软雅黑", 50).into_font().style(FontStyle::Bold); // 设置粗体
            // 将 FontDesc 转换为 TextStyle
            let yhfont = font
                .into_text_style(&root)        //.transform(FontTransform::Rotate180)
                .color(&RED);
        
            // 定义绘图区域
            let mut chart = ChartBuilder::on(&root)
            .caption("三维曲面", yhfont.clone())        //.caption("正弦曲线", ("微软雅黑", 50).into_font())
            .margin(0)
            .build_cartesian_3d(-6.0..6.0, -0.0..10.0, -6.0..6.0)
            .map_err(|e| e.to_string())?;

            // 通过闭包配置投影矩阵
            chart.with_projection(|mut p| {
                p.yaw = yaw as f64/100.00;   // 设置偏航角(绕 Y 轴旋转)
                p.pitch = 0.5; // 设置俯仰角
                p.scale = 0.75;  // 设置缩放比例
                p.into_matrix() // 返回 ProjectionMatrix
            });


            chart
                .configure_axes()
                .light_grid_style(BLACK.mix(0.15))
                .max_light_lines(3)
                .draw()
                .map_err(|e| e.to_string())?;

            chart.draw_series(
                SurfaceSeries::xoz(
                    (-30..=30).map(|x| x as f64 / 5.0),
                    (-30..=30).map(|x| x as f64 / 5.0),
                    sin_sqrt,
                )
                .style_func(&|&v| (VulcanoHSL::get_color(v / (8.0 - (yaw as f64/4.0)%6.0).abs())).into()),
            )
            .map_err(|e| e.to_string())?;
        }

        // Convert the buffer to an image
        let image = RgbImage::from_raw(600, 620, buffer).ok_or("Failed to create image from buffer")?;
        let dynamic_image = DynamicImage::ImageRgb8(image);

        // Convert DynamicImage to ndarray
        let rgb_image = dynamic_image.to_rgb8(); // 转换为 RGB 图像
        let array = Array3::from_shape_fn((rgb_image.height() as usize, rgb_image.width() as usize, 3), |(y, x, c)| {
            rgb_image.get_pixel(x as u32, y as u32)[c]
        });

        // Calculate PTS for the current frame
        let pts = Time::from_secs_f64(frame_duration_sec * yaw as f64);

        // Encode the frame into the video
        encoder.encode(&array, pts).map_err(|e| e.to_string())?;
    }


    // Finish encoding
    encoder.finish().map_err(|e| e.to_string())?;

    // Read the video file and encode it as Base64
    let file_data = std::fs::read(&app_data_dir).map_err(|e| e.to_string())?;
    let base64_data = STANDARD.encode(&file_data);

    // Return the Base64-encoded video data
    Ok(format!("data:video/mp4;base64,{}", base64_data))
}




mod tray;       //导入tray.rs模块
mod mymenu;     //导入mynemu.rs模块
use mymenu::{create_menu, handle_menu_event};

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_opener::init())
        .invoke_handler(tauri::generate_handler![greet, 
            get_db_value, 
            insert_db_item, 
            update_user, 
            del_last_user, 
            send_db_item,
            open_new_window,
            close_window,
            close_main_window,
            show_window,
            insert_div,
            emit_event,
            write_pdt_db,
            send_pdt_db,
            del_last_pdt,
            generate_plot,
            generate_svg_curve,
            python_plot,
            generate_gif_curve,
            generate_3d_surface,
            generate_gif_3d,
            generate_mp4_3d
            ])
        .menu(|app|{create_menu(app)})
        .setup(|app| {

            let main_window = app.get_webview_window("main").unwrap();
            main_window.on_menu_event(move |window, event| handle_menu_event(window, event));
 
            #[cfg(all(desktop))]
            {
                let handle = app.handle();
                tray::create_tray(handle)?;         //设置app系统托盘
            }
            tauri::async_runtime::block_on(async move {
                let db = setup_db(&app).await;         //setup_db(&app:&mut App)返回读写的数据库对象
                app.manage(DbState { db });                   //通过app.manage(DbState{db})把数据库对象传递给state:tauri::State<'_, DbState>
            });
            Ok(())
            })
        .run(tauri::generate_context!())
        .expect("运行Tauri程序的时候出错!");
}

 所有的图片和视频都是加密成base64字符串传递给前端的。

三、其它配置 

主要是涉及一些依赖关系,src-tauri/Cargo.toml文件内容如下:

tauri-plugin-opener = "2"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8.2", features = ["sqlite", "runtime-tokio"] }
tokio = { version ="1", features = ["full"] }
futures = "0.3.31"
log = "0.4.22"
chrono = "0.4.39"
plotters = { version = "0.3.7", features = [] }
base64 = "0.22.1"
image = "0.25.5"
uuid = {version = "1.12.0", features = ["v4"] }
video-rs = { version = "0.10", features = ["ndarray"] }
ndarray = "0.16"

至此,基本上实现了Tauri+Leptos后台图形的绘制及其动画视频的制作,并供前端调用展示。


http://www.kler.cn/a/517583.html

相关文章:

  • 算法每日双题精讲 —— 二分查找(寻找旋转排序数组中的最小值,点名)
  • 二叉搜索树中的众数(力扣501)
  • 82,【6】BUUCTF WEB .[CISCN2019 华东南赛区]Double Secret
  • 【PowerQuery专栏】PowerQuery的M语言函数Access数据库访问
  • Linux命令行配置网络代理
  • Unity——从共享文件夹拉取资源到本地
  • Rust 中的方法与关联函数详解
  • MyBatis最佳实践:动态 SQL
  • ANSYS SimAI
  • leetcode刷题记录(八十一)——236. 二叉树的最近公共祖先
  • 为AI聊天工具添加一个知识系统 之68 详细设计 之9 三种中台和时间度量
  • web前端三大主流框架对比,Angular和React和Vue的区别
  • 【Elasticsearch】如何重新启动_reindex任务?
  • Flutter 与 React 前端框架对比:深入分析与实战示例
  • electron打包客户端在rk3588上支持h265硬解
  • AcWing 3585:三角形的边 ← sort() 函数
  • 矩阵的秩在机器学习中具有广泛的应用
  • 解锁C# EF/EF Core:从入门到进阶的技术飞跃
  • AJAX笔记入门篇
  • 免费高效截图软件(snipaste)附下载链接
  • 亚洲加密市场交易量激增,Safe RWA协议助力 Cobo 与 HQ.xyz 处理超 14.9 亿美元交易
  • 人工智能检测中查全率与查准率的权衡分析
  • Fullcalendar @fullcalendar/react 样式错乱丢失问题和导致页面卡顿崩溃问题
  • Android中Service在新进程中的启动流程3
  • Vue 3 的 setup 函数
  • Gaea项目的挑战与机遇:去中心化AI平台的未来发展