From b88ff96b32cb1b15e941a5f7c657b3b24a4d21aa Mon Sep 17 00:00:00 2001 From: olymrifki Date: Mon, 7 Jul 2025 16:07:14 +0700 Subject: [PATCH] connect to csharp --- .env | 1 + .gitignore | 18 +++ Cargo.lock | 7 ++ Cargo.toml | 1 + README.md | 21 ++++ StitchImg/ImageController.cs | 35 ++++++ StitchImg/ImageSpecification.cs | 9 ++ StitchImg/Program.cs | 51 +++++++++ StitchImg/Properties/launchSettings.json | 41 +++++++ StitchImg/StitchImg.csproj | 19 ++++ StitchImg/appsettings.Development.json | 8 ++ StitchImg/appsettings.json | 9 ++ WebApplication1.sln | 16 +++ global.json | 7 ++ shell.nix | 20 ++++ src/main.rs | 136 ++++++++++++----------- 16 files changed, 336 insertions(+), 63 deletions(-) create mode 100644 .env create mode 100644 README.md create mode 100755 StitchImg/ImageController.cs create mode 100755 StitchImg/ImageSpecification.cs create mode 100755 StitchImg/Program.cs create mode 100755 StitchImg/Properties/launchSettings.json create mode 100755 StitchImg/StitchImg.csproj create mode 100755 StitchImg/appsettings.Development.json create mode 100755 StitchImg/appsettings.json create mode 100755 WebApplication1.sln create mode 100755 global.json create mode 100644 shell.nix diff --git a/.env b/.env new file mode 100644 index 0000000..f8e2bb6 --- /dev/null +++ b/.env @@ -0,0 +1 @@ +ASSET_PATH_RO="/mnt/c/Users/Formulatrix/Documents/Explorations/tiles1705/" diff --git a/.gitignore b/.gitignore index b76f169..9576bc8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,20 @@ /target *.png +bin/ +obj/ +/packages/ +riderModule.iml +/_ReSharper.Caches/ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/modules.xml +/.idea.WebApplication1.iml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/Cargo.lock b/Cargo.lock index 443f261..fefe5a9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -80,6 +80,12 @@ dependencies = [ "litrs", ] +[[package]] +name = "dotenv" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f" + [[package]] name = "fast_image_resize" version = "5.1.4" @@ -412,6 +418,7 @@ dependencies = [ name = "stitcher" version = "0.1.0" dependencies = [ + "dotenv", "fast_image_resize", "futures", "png", diff --git a/Cargo.toml b/Cargo.toml index 2b6d10e..e0e3b4d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] +dotenv = "0.15" png = "0.17.16" tokio = { version = "1.46.0", features = ["full"] } fast_image_resize = "5.1.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..b3eb6df --- /dev/null +++ b/README.md @@ -0,0 +1,21 @@ +Stitch only png image + +## Installation + +0. Install nix +``` +curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate +``` +Follow the installation guide. For complete information: https://github.com/DeterminateSystems/nix-installer + +Then run this (and wait `:)`) +``` +nix-shell --pure +``` +you can `exit` from the shell later. + +1. finally +``` +cd StitchImg/ +cargo run --release & dotnet run -c Release +``` diff --git a/StitchImg/ImageController.cs b/StitchImg/ImageController.cs new file mode 100755 index 0000000..122d07e --- /dev/null +++ b/StitchImg/ImageController.cs @@ -0,0 +1,35 @@ +using System.Net.Sockets; +using System.Text; +using Microsoft.AspNetCore.Mvc; + +namespace StitchImg; + +[ApiController] +[Route("api/image")] +public class ImageController : ControllerBase +{ + [HttpPost("generate")] + public async Task Generate([FromBody] ImageSpecification specification) + { + var serverIp = "127.0.0.1"; + var port = 8080; + + var tcpClient = new TcpClient(); + await tcpClient.ConnectAsync(serverIp, port); + NetworkStream networkStream = tcpClient.GetStream(); + var writer = new BinaryWriter(networkStream); + + string rect = specification.CanvasRect; + byte[] rectBytes = Encoding.UTF8.GetBytes(rect); + writer.Write((ushort)rectBytes.Length); + writer.Write(rectBytes); + writer.Write(specification.CropOffset[0]); + writer.Write(specification.CropSize[0]); + writer.Write(specification.CropOffset[1]); + writer.Write(specification.CropSize[1]); + writer.Write(specification.OutputScale); + writer.Flush(); + + return File( networkStream, "image/png"); + } +} \ No newline at end of file diff --git a/StitchImg/ImageSpecification.cs b/StitchImg/ImageSpecification.cs new file mode 100755 index 0000000..aa2ae00 --- /dev/null +++ b/StitchImg/ImageSpecification.cs @@ -0,0 +1,9 @@ +namespace StitchImg; + +public class ImageSpecification +{ + public string CanvasRect { get; set; } + public IReadOnlyList CropOffset { get; set; } + public IReadOnlyList CropSize { get; set; } + public float OutputScale { get; set; } +} \ No newline at end of file diff --git a/StitchImg/Program.cs b/StitchImg/Program.cs new file mode 100755 index 0000000..2ad147d --- /dev/null +++ b/StitchImg/Program.cs @@ -0,0 +1,51 @@ +using Microsoft.OpenApi.Models; +using Newtonsoft.Json.Serialization; +using Swashbuckle.AspNetCore.SwaggerGen; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddControllers() + .AddNewtonsoftJson(options => + { + options.SerializerSettings.ContractResolver = new DefaultContractResolver + { + NamingStrategy = new SnakeCaseNamingStrategy() + }; + }); + +builder.Services.AddEndpointsApiExplorer(); +builder.Services.AddSwaggerGen(c => + { + c.SchemaFilter(); + }); + +var app = builder.Build(); + +if (app.Environment.IsDevelopment()) +{ + app.UseSwagger(); + app.UseSwaggerUI(); +} + +app.UseHttpsRedirection(); +app.MapControllers(); +app.Run(); + +public class SnakeCaseSchemaFilter : ISchemaFilter +{ + private readonly SnakeCaseNamingStrategy _namingStrategy = new(); + + public void Apply(OpenApiSchema schema, SchemaFilterContext context) + { + if (schema.Properties == null) return; + + var newProps = new Dictionary(); + foreach (var prop in schema.Properties) + { + var snakeName = _namingStrategy.GetPropertyName(prop.Key, false); + newProps[snakeName] = prop.Value; + } + + schema.Properties = newProps; + } +} diff --git a/StitchImg/Properties/launchSettings.json b/StitchImg/Properties/launchSettings.json new file mode 100755 index 0000000..959db08 --- /dev/null +++ b/StitchImg/Properties/launchSettings.json @@ -0,0 +1,41 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:53404", + "sslPort": 44379 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "http://localhost:5031", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "https": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": true, + "launchUrl": "swagger", + "applicationUrl": "https://localhost:7060;http://localhost:5031", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "launchUrl": "swagger", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/StitchImg/StitchImg.csproj b/StitchImg/StitchImg.csproj new file mode 100755 index 0000000..d28d2c1 --- /dev/null +++ b/StitchImg/StitchImg.csproj @@ -0,0 +1,19 @@ + + + + net8.0 + enable + enable + + + + + + + + + + + + + diff --git a/StitchImg/appsettings.Development.json b/StitchImg/appsettings.Development.json new file mode 100755 index 0000000..ff66ba6 --- /dev/null +++ b/StitchImg/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/StitchImg/appsettings.json b/StitchImg/appsettings.json new file mode 100755 index 0000000..4d56694 --- /dev/null +++ b/StitchImg/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/WebApplication1.sln b/WebApplication1.sln new file mode 100755 index 0000000..36aa600 --- /dev/null +++ b/WebApplication1.sln @@ -0,0 +1,16 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StitchImg", "StitchImg\StitchImg.csproj", "{3AE7CACB-316B-4A70-B15C-537A285ADCDA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3AE7CACB-316B-4A70-B15C-537A285ADCDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3AE7CACB-316B-4A70-B15C-537A285ADCDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3AE7CACB-316B-4A70-B15C-537A285ADCDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3AE7CACB-316B-4A70-B15C-537A285ADCDA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/global.json b/global.json new file mode 100755 index 0000000..2ddda36 --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "8.0.0", + "rollForward": "latestMinor", + "allowPrerelease": false + } +} \ No newline at end of file diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000..f7c2359 --- /dev/null +++ b/shell.nix @@ -0,0 +1,20 @@ +{ pkgs ? import + (builtins.fetchTarball { + url = "https://github.com/NixOS/nixpkgs/archive/refs/heads/nixos-24.11.tar.gz"; + }) + { } +}: + +pkgs.mkShell { + name = "dotnet-rust-env"; + + buildInputs = [ + pkgs.dotnetCorePackages.sdk_8_0 + pkgs.rustup + ]; + + shellHook = '' + export PATH=$HOME/.cargo/bin:$PATH + ''; +} + diff --git a/src/main.rs b/src/main.rs index b283b02..dc4e3b5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,67 +3,80 @@ use fr::{PixelType, ResizeAlg, ResizeOptions, Resizer, images::Image}; use futures::future::join_all; use std::{ cell::UnsafeCell, + env, io::Cursor, - io::Write, ops::{Bound, RangeBounds}, + path::Path, }; -use tokio::fs; -// use tokio::net::TcpListener; +use tokio::{ + fs, + io::{AsyncReadExt, AsyncWriteExt}, +}; +use tokio::{io::BufReader, net::TcpListener}; + #[tokio::main] async fn main() -> Result<(), Box> { - // let listener = TcpListener::bind("127.0.0.1:8080").await?; - // println!("Server listening on port 8080"); - // - // loop { - // let (socket, _) = listener.accept().await?; - // tokio::spawn(async move { - // if let Err(e) = handle_client(socket).await { - // eprintln!("Error: {}", e); - // } - // }); - // } - // } - // - // async fn handle_client(socket: tokio::net::TcpStream) -> Result<(), Box> { - // // let file = std::fs::File::create("raw_image_123.png").unwrap(); - // let offset_x = 0.5; - // let size_x = 0.5; - // let offset_y = 0.5; - // let size_y = 0.5; - // let rect = "A1:AE55"; - // let scale = 0.4; - // - // resize_image( - // &mut socket.into_std().unwrap(), - // rect, - // scale, - // offset_x, - // offset_y, - // size_x, - // size_y, - // ) - // .await; - // - // Ok(()) - // } - let timer_start = std::time::Instant::now(); + dotenv::dotenv().ok(); + let listener = TcpListener::bind("127.0.0.1:8080").await?; + println!("Server listening on port 8080"); + loop { + let (socket, _) = listener.accept().await?; + tokio::spawn(async move { + let timer_start = std::time::Instant::now(); + if let Err(e) = handle_client(socket).await { + println!("Error: {}", e); + } else { + println!("Data sent"); + } + let duration = timer_start.elapsed(); + println!("Operation completed in: {:.2?}", duration); + }); + } +} - let file = std::fs::File::create("raw_image_123.png").unwrap(); - let offset_x = 0.5; - let size_x = 0.5; - let offset_y = 0.5; - let size_y = 0.5; - let rect = "A1:AE55"; - let scale = 0.4; +async fn handle_client( + mut socket: tokio::net::TcpStream, +) -> Result<(), Box> { + println!("Incoming request"); + let mut reader = BufReader::new(&mut socket); - resize_image(file, rect, scale, offset_x, offset_y, size_x, size_y).await; + let rect_len = reader.read_u16_le().await?; + let mut rect_buf = vec![0u8; rect_len as usize]; + reader.read_exact(&mut rect_buf).await?; + let rect = String::from_utf8(rect_buf)?; + + let offset_x = reader.read_f32_le().await?; + let size_x = reader.read_f32_le().await?; + let offset_y = reader.read_f32_le().await?; + let size_y = reader.read_f32_le().await?; + let scale = reader.read_f32_le().await?; + + println!("Parsed values:"); + println!(" rect: {rect}"); + println!(" offset_x: {offset_x}"); + println!(" size_x: {size_x}"); + println!(" offset_y: {offset_y}"); + println!(" size_y: {size_y}"); + println!(" scale: {scale}"); + + let mut result_buffer = Vec::new(); + stitch_and_resize_image( + &mut result_buffer, + &rect, + scale, + offset_x, + offset_y, + size_x, + size_y, + ) + .await; + + socket.write_all(&result_buffer).await.unwrap(); - let duration = timer_start.elapsed(); - println!("Operation completed in: {:.2?}", duration); Ok(()) } -async fn resize_image( - out: impl Write, +async fn stitch_and_resize_image( + out: &mut Vec, rect: &str, scale: f32, offset_x: f32, @@ -125,8 +138,6 @@ async fn resize_image( } let results = join_all(handles).await; - let writer = std::io::BufWriter::new(out); - let start_x = (offset_x * canvas_width as f32).trunc() as usize; let start_y = (offset_y * canvas_height as f32).trunc() as usize; let end_x = ((offset_x + size_x) * canvas_width as f32).trunc() as usize; @@ -136,16 +147,15 @@ async fn resize_image( let crop_height = end_y - start_y; let mut encoder = png::Encoder::new( - writer, + Cursor::new(out), crop_width.try_into().unwrap(), crop_height.try_into().unwrap(), ); encoder.set_color(png::ColorType::Rgb); encoder.set_depth(png::BitDepth::Eight); - - println!("Writing img"); - let mut png_writer = encoder.write_header().unwrap(); - png_writer + encoder + .write_header() + .unwrap() .write_image_data(&crop_rgb_image( &canvas.to_vec(), (actual_size * row_count) as usize, @@ -202,11 +212,11 @@ async fn add_to_canvas( canvas_height: usize, ) { let mut resizer = Resizer::new(); - let base_path = "/mnt/c/Users/Formulatrix/Documents/Explorations/WebApplication1/tiles1705/"; - let filename = format!("{}{}{}.png", base_path, number_to_column(i), j); - // println!("{filename}"); + let env = &env::var("ASSET_PATH_RO").unwrap(); + let img_path = Path::new(&env); + let filename = format!("{}{}.png", number_to_column(i), j); - let data = Cursor::new(fs::read(filename).await.unwrap()); // .unwrap(); + let data = Cursor::new(fs::read(img_path.join(filename)).await.unwrap()); let decoder = png::Decoder::new(data); let mut reader = decoder.read_info().unwrap(); let mut buf = vec![0; reader.output_buffer_size()]; @@ -225,7 +235,7 @@ async fn add_to_canvas( ) .unwrap(); place_image( - &canvas, + canvas, canvas_width, canvas_height, &img.into_vec(),