WIP: image processing library (or libraries?) #12

Draft
schrottkatze wants to merge 15 commits from schrottkatze/iowo:proc-libs into main
4 changed files with 356 additions and 3 deletions
Showing only changes of commit 2d537cc4ee - Show all commits

View file

@ -56,17 +56,20 @@ fn main() {
mod dev {
use clap::Subcommand;
use prowocessing::experimental::enum_based::{Pipeline, PipelineBuilder};
use prowocessing::experimental::trait_based::DataType;
#[derive(Subcommand)]
pub(crate) enum DevCommands {
Enums { test_str: String },
Add { num0: i32, num1: i32 },
}
impl DevCommands {
pub fn run(self) {
match self {
DevCommands::Enums { test_str } => {
use prowocessing::experimental::enum_based::PipelineBuilder;
let upr = PipelineBuilder::new()
.insert(prowocessing::experimental::enum_based::Instruction::Uppercase)
.build();
@ -77,6 +80,15 @@ mod dev {
println!("Upr: {}", upr.run(test_str.clone()));
println!("Lwr: {}", lwr.run(test_str.clone()));
}
DevCommands::Add { num0, num1 } => {
use prowocessing::experimental::trait_based::PipelineBuilder;
let pipe = PipelineBuilder::new().add(1).stringify().build();
println!(
"{:?}",
multisamplednight marked this conversation as resolved Outdated

All of these dev commands seem like they'd belong into tests using #[test] instead.

All of these dev commands seem like they'd belong into tests using `#[test]` instead.

that's what i already mentioned, that i prefer playing around like that

sure, can do that, but i much prefer having them run like that and output there so i can see it more or less interactively.

planning to do that, but the experimental things are all termporary anyway.

that's what i already mentioned, that i prefer playing around like that sure, can do that, but i much prefer having them run like that and output there so i can see it more or less interactively. planning to do that, but the `experimental` things are all termporary anyway.

sure, can do that, but i much prefer having them run like that and output there so i can see it more or less interactively.

They're still just as interactive if one were to use cargo test --no-capture <test_name>.

planning to do that, but the experimental things are all termporary [sic] anyway.

They're what this PR introduces and what you requested my review on.

> sure, can do that, but i much prefer having them run like that and output there so i can see it more or less interactively. They're still just as interactive if one were to use `cargo test --no-capture <test_name>`. > planning to do that, but the `experimental` things are all termporary [sic] anyway. They're what this PR introduces and what you requested my review on.

They are indeed not what this PR introduces, those will not be in the PR once it's complete. this is basically just a working copy/scratchpad state for now, and i'll be purging it once the actual library is done which is then what will be introduced by this PR.

They are indeed not what this PR introduces, those will not be in the PR once it's complete. this is basically just a working copy/scratchpad state for now, and i'll be purging it once the actual library is done which is then what will be introduced by this PR.

Fair enough.

Fair enough.
pipe.run(vec![num0.into(), num1.into()].into()).into_inner()[0]
);
}
}
}
}

View file

@ -1 +1,3 @@
pub mod enum_based;
pub mod trait_based;

View file

@ -0,0 +1,339 @@
use self::{
numops::{Add, Stringify, Subtract},
strops::{Concatenate, Lower, Upper},
};
trait PipelineElement {
fn runner(&self) -> fn(&Inputs) -> Outputs;
fn signature(&self) -> ElementIo;
}
struct ElementIo {
pub inputs: Vec<DataType>,
pub outputs: Vec<DataType>,
}
// TODO:
// - Bind additional inputs if instruction has more then one and is passd without any additional
// - allow binding to pointers to other pipelines?
// - allow referencing earlier data
pub struct PipelineBuilder {
elements: Vec<Box<dyn PipelineElement>>,
}
pub struct Pipeline {
runners: Vec<fn(&Inputs) -> Outputs>,
}
impl Pipeline {
pub fn run(&self, inputs: Inputs) -> Outputs {
let mut out: Outputs = inputs.into();
for runner in &self.runners {
out = runner(&(&out).into());
}
out
}
}
impl PipelineBuilder {
pub fn new() -> Self {
Self {
elements: Vec::new(),
}
}
fn insert<T: PipelineElement + 'static>(mut self, el: T) -> Self {
if let Some(previous_item) = self.elements.last() {
assert_eq!(
previous_item.signature().outputs[0],
el.signature().inputs[0]
);
}
self.elements.push(Box::new(el));
self
}
#[must_use]
pub fn concatenate(self, sec: String) -> Self {
self.insert(Concatenate(sec))
}
#[must_use]
pub fn upper(self) -> Self {
self.insert(Upper)
}
#[must_use]
pub fn lower(self) -> Self {
self.insert(Lower)
}
#[must_use]
#[allow(
clippy::should_implement_trait,
reason = "is not equivalent to addition"
)]
pub fn add(self, sec: i32) -> Self {
self.insert(Add(sec))
}
#[must_use]
pub fn subtract(self, sec: i32) -> Self {
self.insert(Subtract(sec))
}
#[must_use]
pub fn stringify(self) -> Self {
self.insert(Stringify)
}
pub fn build(&self) -> Pipeline {
let mut r = Vec::new();
self.elements.iter().for_each(|el| r.push(el.runner()));
Pipeline { runners: r }
}
}
impl Default for PipelineBuilder {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone, Copy)]
pub enum Data<'a> {
String(&'a str),
Int(i32),
}
impl Data<'_> {
pub fn to_owned_data(&self) -> OwnedData {
match self {
Data::String(s) => (*s).to_owned().into(),
Data::Int(i) => (*i).into(),
}
}
}
impl<'a> From<&'a str> for Data<'a> {
fn from(value: &'a str) -> Self {
Self::String(value)
}
}
impl From<i32> for Data<'_> {
fn from(value: i32) -> Self {
Self::Int(value)
}
}
impl<'a> From<&'a OwnedData> for Data<'a> {
fn from(value: &'a OwnedData) -> Self {
match value {
OwnedData::String(s) => Data::String(s),
OwnedData::Int(i) => Data::Int(*i),
}
}
}
#[derive(Clone, Debug)]
pub enum OwnedData {
String(String),
Int(i32),
}
impl From<String> for OwnedData {
fn from(value: String) -> Self {
Self::String(value)
}
}
impl From<i32> for OwnedData {
fn from(value: i32) -> Self {
Self::Int(value)
}
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum DataType {
String,
Int,
}
pub struct Inputs<'a>(Vec<Data<'a>>);
impl<'a> Inputs<'a> {
fn inner(&self) -> Vec<Data<'a>> {
self.0.clone()
}
}
impl<'a> From<Vec<Data<'a>>> for Inputs<'a> {
fn from(value: Vec<Data<'a>>) -> Self {
Self(value)
}
}
impl<'a, T: Into<Data<'a>>> From<T> for Inputs<'a> {
fn from(value: T) -> Self {
Self(vec![value.into()])
}
}
impl<'a> From<&'a Outputs> for Inputs<'a> {
fn from(value: &'a Outputs) -> Self {
Self(value.0.iter().map(std::convert::Into::into).collect())
}
}
pub struct Outputs(Vec<OwnedData>);
impl Outputs {
pub fn into_inner(self) -> Vec<OwnedData> {
self.0
}
}
impl From<Vec<OwnedData>> for Outputs {
fn from(value: Vec<OwnedData>) -> Self {
Self(value)
}
}
impl<T: Into<OwnedData>> From<T> for Outputs {
fn from(value: T) -> Self {
Self(vec![value.into()])
}
}
impl From<Inputs<'_>> for Outputs {
fn from(value: Inputs) -> Self {
Self(
value
.0
.into_iter()
.map(|i: Data<'_>| Data::to_owned_data(&i))
.collect(),
)
}
}
mod strops {
use super::{Data, DataType, ElementIo, Inputs, Outputs, PipelineElement};
pub struct Concatenate(pub String);
impl PipelineElement for Concatenate {
fn runner(&self) -> fn(&Inputs) -> Outputs {
|input| {
if let [Data::String(s0), Data::String(s1), ..] = input.inner()[..] {
format!("{s0}{s1}").into()
} else {
panic!("Invalid data passed")
}
}
}
fn signature(&self) -> ElementIo {
ElementIo {
inputs: vec![DataType::String, DataType::String],
outputs: vec![DataType::String],
}
}
}
pub struct Upper;
impl PipelineElement for Upper {
fn runner(&self) -> fn(&Inputs) -> Outputs {
|input| {
if let [Data::String(s), ..] = input.inner()[..] {
s.to_uppercase().into()
} else {
panic!("Invalid data passed")
}
}
}
fn signature(&self) -> ElementIo {
ElementIo {
inputs: vec![DataType::String],
outputs: vec![DataType::String],
}
}
}
pub struct Lower;
impl PipelineElement for Lower {
fn runner(&self) -> fn(&Inputs) -> Outputs {
|input| {
if let [Data::String(s), ..] = input.inner()[..] {
s.to_lowercase().into()
} else {
panic!("Invalid data passed")
}
}
}
fn signature(&self) -> ElementIo {
ElementIo {
inputs: vec![DataType::String],
outputs: vec![DataType::String],
}
}
}
}
mod numops {
use core::panic;
use super::{Data, DataType, ElementIo, Inputs, Outputs, PipelineElement};
pub struct Add(pub i32);
impl PipelineElement for Add {
fn runner(&self) -> fn(&Inputs) -> Outputs {
|input| {
if let [Data::Int(i0), Data::Int(i1), ..] = input.inner()[..] {
(i0 + i1).into()
} else {
panic!("Invalid data passed")
}
}
}
fn signature(&self) -> ElementIo {
ElementIo {
inputs: vec![DataType::Int, DataType::Int],
outputs: vec![DataType::Int],
}
}
}
pub struct Subtract(pub i32);
impl PipelineElement for Subtract {
fn runner(&self) -> fn(&Inputs) -> Outputs {
|input| {
if let [Data::Int(i0), Data::Int(i1), ..] = input.inner()[..] {
(i0 + i1).into()
} else {
panic!("Invalid data passed")
}
}
}
fn signature(&self) -> ElementIo {
ElementIo {
inputs: vec![DataType::Int, DataType::Int],
outputs: vec![DataType::Int],
}
}
}
pub struct Stringify;
impl PipelineElement for Stringify {
fn runner(&self) -> fn(&Inputs) -> Outputs {
|input| {
if let [Data::Int(int), ..] = input.inner()[..] {
int.to_string().into()
} else {
panic!("Invalid data passed")
}
}
}
fn signature(&self) -> ElementIo {
ElementIo {
inputs: vec![DataType::Int],
outputs: vec![DataType::String],
}
}
}
}

View file

@ -2,8 +2,7 @@
//!
//! One of the design goals for this library is, however, to be a simple, generic image processing library.
//! For now, it's just indev... lets see what comes of it!
use experimental::enum_based::{Instruction, PipelineBuilder};
#![feature(lint_reasons)]
/// just some experiments, to test whether the architecture i want is even possible (or how to do it). probably temporary.
/// Gonna first try string processing...
@ -11,6 +10,7 @@ pub mod experimental;
#[test]
fn test_enums() {
use crate::experimental::enum_based::{Instruction, PipelineBuilder};
let builder = PipelineBuilder::new().insert(Instruction::Uppercase);
let upr = builder.build();
let upr_lowr = builder.insert(Instruction::Lowercase).build();