diff --git a/.gitignore b/.gitignore index bed43ed36..9459e8a3a 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ build-emscripten build-wasi src/io/internal/ImageIOIndex.ts src/io/internal/MeshIOIndex.ts +wasi-build/ dist/* !dist/dicom diff --git a/packages/core/java/.gitignore b/packages/core/java/.gitignore new file mode 100644 index 000000000..10d81e8c6 --- /dev/null +++ b/packages/core/java/.gitignore @@ -0,0 +1,4 @@ +/.classpath +/.project +/.settings/ +/target/ diff --git a/packages/core/java/pom.xml b/packages/core/java/pom.xml new file mode 100644 index 000000000..1e250da3c --- /dev/null +++ b/packages/core/java/pom.xml @@ -0,0 +1,111 @@ + + + 4.0.0 + + + org.scijava + pom-scijava + 35.1.1 + + + org.itk.wasm + itk-wasm + 0.1.0-SNAPSHOT + + itk-wasm for Java + Java interface to itk-wasm WebAssembly modules. + https://github.com/InsightSoftwareConsortium/itk-wasm + 2023 + + ITK + https://itk.org/ + + + + The Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0 + + + + + + Matt McCormick + https://github.com/thewtex + thewtex + + + + + Curtis Rueden + https://github.com/ctrueden + ctrueden + + + + + + ITK Forum + https://discourse.itk.org/ + + + + + scm:https://github.com/InsightSoftwareConsortium/itk-wasm + scm:git@github.com:InsightSoftwareConsortium/itk-wasm + https://github.com/InsightSoftwareConsortium/itk-wasm + + + GitHub Issues + https://github.com/InsightSoftwareConsortium/itk-wasm/issues + + + GitHub Actions + https://github.com/InsightSoftwareConsortium/itk-wasm/actions + + + + org.itk.wasm.Main + + apache_v2 + ITK developers. + Java bindings for itk-wasm. + + 0.14.0 + + + + + io.github.kawamuray.wasmtime + wasmtime-java + ${wasmtime-java.version} + + + net.imglib2 + imglib2 + + + net.java.dev.jna + jna + + + net.java.dev.jna + jna-platform + + + com.google.code.gson + gson + + + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + diff --git a/packages/core/java/src/main/java/org/itk/wasm/BinaryFile.java b/packages/core/java/src/main/java/org/itk/wasm/BinaryFile.java new file mode 100644 index 000000000..769f1ab48 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/BinaryFile.java @@ -0,0 +1,15 @@ +package org.itk.wasm; + +import java.nio.file.Path; + +public class BinaryFile { + public PurePosixPath path; + + public BinaryFile(PurePosixPath path) { + this.path = path; + } + + public BinaryFile(Path path) { + this(new PurePosixPath(path)); + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/BinaryStream.java b/packages/core/java/src/main/java/org/itk/wasm/BinaryStream.java new file mode 100644 index 000000000..617b33e5c --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/BinaryStream.java @@ -0,0 +1,28 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +public class BinaryStream { + public byte[] data; + + public BinaryStream(byte[] data) { + this.data = data; + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/FloatTypes.java b/packages/core/java/src/main/java/org/itk/wasm/FloatTypes.java new file mode 100644 index 000000000..d03f6fb96 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/FloatTypes.java @@ -0,0 +1,18 @@ +package org.itk.wasm; + +public enum FloatTypes { + Float32("float32"), + Float64("float64"), + SpacePrecisionType("float64"); + + private final String value; + + FloatTypes(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/IO.java b/packages/core/java/src/main/java/org/itk/wasm/IO.java new file mode 100644 index 000000000..002b4bf6c --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/IO.java @@ -0,0 +1,29 @@ +package org.itk.wasm; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class IO { + + public IO() { } + + public static byte[] readBytes(String path) throws IOException { + //try (InputStream is = Main.class.getResourceAsStream(filename)) { + try (InputStream is = new FileInputStream(path)) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] buf = new byte[16384]; + while ((nRead = is.read(buf, 0, buf.length)) != -1) { + buffer.write(buf, 0, nRead); + } + return buffer.toByteArray(); + } + } + + public static String readString(String path) throws IOException { + return new String(readBytes(path), StandardCharsets.UTF_8); + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/Image.java b/packages/core/java/src/main/java/org/itk/wasm/Image.java new file mode 100644 index 000000000..3039fc2e0 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/Image.java @@ -0,0 +1,69 @@ +package org.itk.wasm; + +import java.util.*; + +public class Image { + public ImageType imageType; + public String name; + public List origin; + public List spacing; + public double[][] direction; + public List size; + public Map metadata; + public double[] data; + + public Image() { + this.imageType = new ImageType(); + this.name = "Image"; + this.origin = new ArrayList<>(); + this.spacing = new ArrayList<>(); + this.direction = new double[0][0]; + this.size = new ArrayList<>(); + this.metadata = new HashMap<>(); + this.data = null; + } + + public void postInit() { + if (imageType instanceof Map) { + imageType = new ImageType(); + Map imageTypeMap = asdict(imageType); + // Set values from the map to the corresponding fields in ImageType + // Example: imageType.setDimension((int) imageTypeMap.get("dimension")); + // Add similar code for other fields + } + + int dimension = imageType.dimension; + if (origin.isEmpty()) { + for (int i = 0; i < dimension; i++) { + origin.add(0.0); + } + } + + if (spacing.isEmpty()) { + for (int i = 0; i < dimension; i++) { + spacing.add(1.0); + } + } + + if (direction.length == 0) { + direction = new double[dimension][dimension]; + for (int i = 0; i < dimension; i++) { + for (int j = 0; j < dimension; j++) { + if (i == j) { + direction[i][j] = 1.0; + } else { + direction[i][j] = 0.0; + } + } + } + } + + if (size.isEmpty()) { + for (int i = 0; i < dimension; i++) { + size.add(1); + } + } + } + + // Add getters and setters for the fields +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/ImageType.java b/packages/core/java/src/main/java/org/itk/wasm/ImageType.java new file mode 100644 index 000000000..b2e74479e --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/ImageType.java @@ -0,0 +1,17 @@ +package org.itk.wasm; + +public class ImageType { + public int dimension; + public IntTypes componentType; + public PixelType pixelType; + public int components; + + public ImageType() { + this.dimension = 2; + this.componentType = IntTypes.UInt8; + this.pixelType = PixelType.Scalar; + this.components = 1; + } + + // Add getters and setters for the fields +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/IntTypes.java b/packages/core/java/src/main/java/org/itk/wasm/IntTypes.java new file mode 100644 index 000000000..791cabe48 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/IntTypes.java @@ -0,0 +1,27 @@ +package org.itk.wasm; + +public enum IntTypes { + Int8("int8"), + UInt8("uint8"), + Int16("int16"), + UInt16("uint16"), + Int32("int32"), + UInt32("uint32"), + Int64("int64"), + UInt64("uint64"), + SizeValueType("uint64"), + IdentifierType("uint64"), + IndexValueType("int64"), + OffsetValueType("int64"); + + private final String value; + + IntTypes(String value) { + this.value = value; + } + + @Override + public String toString() { + return value; + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/InterfaceType.java b/packages/core/java/src/main/java/org/itk/wasm/InterfaceType.java new file mode 100644 index 000000000..bad6226e1 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/InterfaceType.java @@ -0,0 +1,31 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +public enum InterfaceType { + TextFile, + BinaryFile, + TextStream, + BinaryStream, + Image, + Mesh, + PolyData, + JsonObject +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/JsonObject.java b/packages/core/java/src/main/java/org/itk/wasm/JsonObject.java new file mode 100644 index 000000000..a31d4a7b4 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/JsonObject.java @@ -0,0 +1,19 @@ +package org.itk.wasm; + +import java.util.Map; + +public class JsonObject { + private Map data; + + public JsonObject(Map data) { + this.data = data; + } + + public Map getData() { + return data; + } + + public void setData(Map data) { + this.data = data; + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/Main.java b/packages/core/java/src/main/java/org/itk/wasm/Main.java new file mode 100644 index 000000000..d470415e9 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/Main.java @@ -0,0 +1,77 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +import io.github.kawamuray.wasmtime.Config; +import io.github.kawamuray.wasmtime.Engine; +import io.github.kawamuray.wasmtime.Extern; +import io.github.kawamuray.wasmtime.Linker; +import io.github.kawamuray.wasmtime.Module; +import io.github.kawamuray.wasmtime.Store; +import io.github.kawamuray.wasmtime.WasmFunctions; +import io.github.kawamuray.wasmtime.WasmFunctions.Consumer0; +import io.github.kawamuray.wasmtime.wasi.WasiCtx; +import io.github.kawamuray.wasmtime.wasi.WasiCtxBuilder; + +import java.io.ByteArrayOutputStream; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.InputStream; + +public class Main { + public static void main(String... args) throws IOException { + // Configure the initial compilation environment, creating the global + // `Store` structure. Note that you can also tweak configuration settings + // with a `Config` and an `Engine` if desired. + System.err.println("Initializing..."); + Config config = new Config(); + try ( + Engine engine = new Engine(config); + Linker linker = new Linker(engine); + Module module = Module.fromBinary(engine, readBytes("../test/data/input/stdout-stderr-test.wasi.wasm"))) + { + // Here we handle the imports of the module, which in this case is our + // `HelloCallback` type and its associated implementation of `Callback. + System.err.println("Creating callback..."); + + WasiCtx wasi = new WasiCtxBuilder().inheritStdout().inheritStderr().build(); + Store store = new Store<>(null, engine, wasi); + WasiCtx.addToLinker(linker); + String moduleName = "this-can-be-anything"; + linker.module(store, moduleName, module); + Extern extern = linker.get(store, moduleName, "").get(); + Consumer0 doWork = WasmFunctions.consumer(store, extern.func()); + doWork.accept(); + } + } + + private static byte[] readBytes(String filename) throws IOException { + //try (InputStream is = Main.class.getResourceAsStream(filename)) { + try (InputStream is = new FileInputStream(filename)) { + ByteArrayOutputStream buffer = new ByteArrayOutputStream(); + int nRead; + byte[] buf = new byte[16384]; + while ((nRead = is.read(buf, 0, buf.length)) != -1) { + buffer.write(buf, 0, nRead); + } + return buffer.toByteArray(); + } + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/Pipeline.java b/packages/core/java/src/main/java/org/itk/wasm/Pipeline.java new file mode 100644 index 000000000..6b90d419f --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/Pipeline.java @@ -0,0 +1,301 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +import static io.github.kawamuray.wasmtime.WasmValType.I32; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; + +import io.github.kawamuray.wasmtime.Config; +import io.github.kawamuray.wasmtime.Engine; +import io.github.kawamuray.wasmtime.Extern; +import io.github.kawamuray.wasmtime.Linker; +import io.github.kawamuray.wasmtime.Memory; +import io.github.kawamuray.wasmtime.Module; +import io.github.kawamuray.wasmtime.Store; +import io.github.kawamuray.wasmtime.WasmFunctions; +import io.github.kawamuray.wasmtime.WasmFunctions.Consumer0; +import io.github.kawamuray.wasmtime.WasmFunctions.Consumer1; +import io.github.kawamuray.wasmtime.WasmFunctions.Function0; +import io.github.kawamuray.wasmtime.WasmFunctions.Function1; +import io.github.kawamuray.wasmtime.WasmFunctions.Function2; +import io.github.kawamuray.wasmtime.WasmFunctions.Function3; +import io.github.kawamuray.wasmtime.WasmFunctions.Function4; +import io.github.kawamuray.wasmtime.wasi.WasiCtx; +import io.github.kawamuray.wasmtime.wasi.WasiCtxBuilder; + +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class Pipeline { + private static int instanceID = 0; + + private Config config; + private Engine engine; + private Linker linker; + private Module module; + + public Pipeline(Path path) throws IOException { + this(path.toString()); + } + + public Pipeline(String path) throws IOException { + this(IO.readBytes(path)); + } + + public Pipeline(byte[] wasmBytes) { + config = new Config(); + //config.wasmBulkMemory(true); // <-- This method causes a mysterious ClassNotFoundException + //config.wasmSimd(true); // <-- This method causes a mysterious ClassNotFoundException + //config.wasmMemory64(true); // <-- This method does not exist + engine = new Engine(config); + + linker = new Linker(engine); + //linker.allowShadowing(true); + module = new Module(engine, wasmBytes); + + WasiCtx.addToLinker(linker); + } + + public List> run(List args) { + return run(args, Collections.emptyList(), Collections.emptyList()); + } + + public List> run(List args, List> outputs, List> inputs) { + try (RunInstance ri = new RunInstance(args, outputs, inputs)) { + int returnCode = ri.delayedStart(); + if (returnCode != 0) throw new RuntimeException("Non-zero return code: " + returnCode); //TEMP + + List> populatedOutputs = new ArrayList<>(); + if (!outputs.isEmpty() && returnCode == 0) { + for (int index = 0; index < outputs.size(); index++) { + PipelineOutput output = outputs.get(index); + if (output.type == InterfaceType.TextStream) { + int dataPtr = ri.outputArrayAddress(0, index, 0); + int dataLen = ri.outputArraySize(0, index, 0); + byte[] dataBytes = ri.wasmTimeLift(dataPtr, dataLen); + String dataString = str(dataBytes); + TextStream textStream = new TextStream(dataString); + populatedOutputs.add(new PipelineOutput<>(InterfaceType.TextStream, textStream)); + } else if (output.type == InterfaceType.BinaryStream) { + int dataPtr = ri.outputArrayAddress(0, index, 0); + int dataLen = ri.outputArraySize(0, index, 0); + byte[] dataBytes = ri.wasmTimeLift(dataPtr, dataLen); + BinaryStream binaryStream = new BinaryStream(dataBytes); + populatedOutputs.add(new PipelineOutput<>(InterfaceType.BinaryStream, binaryStream)); + } else { + throw new IllegalArgumentException("Unexpected/not yet supported output.type " + output.type); + } + } + } + + return populatedOutputs; + } + } + + private static String str(byte[] bytes) { + return new String(bytes, StandardCharsets.UTF_8); + } + private static byte[] bytes(String str) { + return str.getBytes(StandardCharsets.UTF_8); + } + + private class RunInstance implements AutoCloseable { + + private final Store store; + private final String moduleName; + + private final Consumer0 main; + private final Consumer0 initialize; + private final Function0 delayedStart; + private final Consumer1 delayedExit; + private final Function4 inputArrayAlloc; + private final Function3 inputJsonAlloc; + private final Function2 outputJsonAddress; + private final Function2 outputJsonSize; + private final Function3 outputArrayAddress; + private final Function3 outputArraySize; + private final Consumer0 freeAll; + private final Memory memory; + + public RunInstance(List args, List> outputs, + List> inputs) + { + WasiCtx wasiConfig = new WasiCtxBuilder() + //.inheritEnv() + .inheritStderr() + //.inheritStdin() + .inheritStdout() + //.args(args) + .build(); + + + Set preopenDirectories = new HashSet<>(); + for (PipelineInput input : inputs) { + if (input.type == InterfaceType.TextFile) { + PurePosixPath path = ((TextFile) input.data).path; + preopenDirectories.add(path.getParent().toString()); + } + if (input.type == InterfaceType.BinaryFile) { + PurePosixPath path = ((BinaryFile) input.data).path; + preopenDirectories.add(path.getParent().toString()); + } + } + for (PipelineOutput output : outputs) { + if (output.type == InterfaceType.TextFile) { + PurePosixPath path = ((TextFile) output.data).path; + preopenDirectories.add(path.getParent().toString()); + } + if (output.type == InterfaceType.BinaryFile) { + PurePosixPath path = ((BinaryFile) output.data).path; + preopenDirectories.add(path.getParent().toString()); + } + } + + for (String preopen : preopenDirectories) { + Path p = Paths.get(preopen); + wasiConfig.pushPreopenDir(p, preopen); + } + + // Instantiate the module. + store = new Store<>(null, engine, wasiConfig); + moduleName = "instance" + instanceID++; + linker.module(store, moduleName, module); + + main = consumer0(store, ""); + initialize = consumer0(store, "_initialize"); + delayedStart = func0(store, "itk_wasm_delayed_start"); + delayedExit = consumer1(store, "itk_wasm_delayed_exit"); + inputArrayAlloc = func4(store, "itk_wasm_input_array_alloc"); + inputJsonAlloc = func3(store, "itk_wasm_input_json_alloc"); + outputJsonAddress = func2(store, "itk_wasm_output_json_address"); + outputJsonSize = func2(store, "itk_wasm_output_json_size"); + outputArrayAddress = func3(store, "itk_wasm_output_array_address"); + outputArraySize = func3(store, "itk_wasm_output_array_size"); + freeAll = consumer0(store, "itk_wasm_free_all"); + memory = extern(store, "memory").memory(); + } + + public Integer delayedStart() { return delayedStart.call(); } + public void delayedExit(Integer i) { delayedExit.accept(i); } + public Integer inputArrayAlloc(Integer i1, Integer i2, Integer i3, Integer i4) { return inputArrayAlloc.call(i1, i2, i3, i4); } + public Integer inputJsonAlloc(Integer i1, Integer i2, Integer i3) { return inputJsonAlloc.call(i1, i2, i3); } + public Integer outputJsonAddress(Integer i1, Integer i2) { return outputJsonAddress.call(i1, i2); } + public Integer outputJsonSize(Integer i1, Integer i2) { return outputJsonSize.call(i1, i2); } + public Integer outputArrayAddress(Integer i1, Integer i2, Integer i3) { return outputArrayAddress.call(i1, i2, i3); } + public Integer outputArraySize(Integer i1, Integer i2, Integer i3) { return outputArraySize.call(i1, i2, i3); } + public void freeAll() { freeAll.accept(); } + + public ByteBuffer memoryBuffer(int offset, int length) { + ByteBuffer buffer = memory.buffer(store); + buffer.position(offset); + buffer.limit(length); + return buffer.slice(); + } + public int memorySize() { return memory.size(store); } + + @Override + public void close() { + store.close(); + } + + private byte[] wasmTimeLift(int offset, int length) { + if (offset + length > memorySize()) { + throw new IndexOutOfBoundsException("Attempting to lift out of bounds"); + } + ByteBuffer byteBuffer = memoryBuffer(offset, length); + byte[] data = new byte[byteBuffer.remaining()]; + byteBuffer.get(data); + return data; + } + + private void wasmTimeLower(int offset, byte[] data) { + int size = data.length; + if (offset + size > memorySize()) { + throw new IndexOutOfBoundsException("Attempting to lower out of bounds"); + } + ByteBuffer byteBuffer = memoryBuffer(offset, size); + byteBuffer.put(data); + } + + private int setInputArray(byte[] dataArray, int inputIndex, int subIndex) { + int dataPtr = 0; + if (dataArray != null) { + dataPtr = inputArrayAlloc(0, inputIndex, subIndex, dataArray.length); + wasmTimeLower(dataPtr, dataArray); + } + return dataPtr; + } + + private void setInputJson(Map dataObject, int inputIndex) { + Gson gson = new GsonBuilder().create(); + JsonElement jsonElement = gson.toJsonTree(dataObject); + byte[] dataJson = bytes(jsonElement.toString()); + int jsonPtr = inputJsonAlloc(0, inputIndex, dataJson.length); + wasmTimeLower(jsonPtr, dataJson); + } + + private Map getOutputJson(int outputIndex) { + int jsonPtr = outputJsonAddress(0, outputIndex); + int jsonLen = outputJsonSize(0, outputIndex); + byte[] jsonBytes = wasmTimeLift(jsonPtr, jsonLen); + String jsonString = str(jsonBytes); + Gson gson = new GsonBuilder().create(); + return gson.fromJson(jsonString, new TypeToken>() {}); + } + + private Extern extern(Store store, String name) { + return linker.get(store, moduleName, name).get(); + } + private Consumer0 consumer0(Store store, String name) { + return WasmFunctions.consumer(store, extern(store, name).func()); + } + private Consumer1 consumer1(Store store, String name) { + return WasmFunctions.consumer(store, extern(store, name).func(), I32); + } + private Function0 func0(Store store, String name) { + return WasmFunctions.func(store, extern(store, name).func(), I32); + } + private Function1 func1(Store store, String name) { + return WasmFunctions.func(store, extern(store, name).func(), I32, I32); + } + private Function2 func2(Store store, String name) { + return WasmFunctions.func(store, extern(store, name).func(), I32, I32, I32); + } + private Function3 func3(Store store, String name) { + return WasmFunctions.func(store, extern(store, name).func(), I32, I32, I32, I32); + } + private Function4 func4(Store store, String name) { + return WasmFunctions.func(store, extern(store, name).func(), I32, I32, I32, I32, I32); + } + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/PipelineInput.java b/packages/core/java/src/main/java/org/itk/wasm/PipelineInput.java new file mode 100644 index 000000000..61954dcc4 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/PipelineInput.java @@ -0,0 +1,31 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +public class PipelineInput { + public InterfaceType type; + public T data; + public String path; + + public PipelineInput(InterfaceType type, T data) { + this.type = type; + this.data = data; + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/PipelineOutput.java b/packages/core/java/src/main/java/org/itk/wasm/PipelineOutput.java new file mode 100644 index 000000000..4fc8ef5d7 --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/PipelineOutput.java @@ -0,0 +1,34 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +public class PipelineOutput { + public InterfaceType type; + public T data; + public String path; + + public PipelineOutput(InterfaceType type) { + this.type = type; + } + public PipelineOutput(InterfaceType type, T data) { + this.type = type; + this.data = data; + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/PixelType.java b/packages/core/java/src/main/java/org/itk/wasm/PixelType.java new file mode 100644 index 000000000..fc2b12bca --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/PixelType.java @@ -0,0 +1,20 @@ +package org.itk.wasm; + +public enum PixelType { + Unknown, + Scalar, + RGB, + RGBA, + Offset, + Vector, + Point, + CovariantVector, + SymmetricSecondRankTensor, + DiffusionTensor3D, + Complex, + FixedArray, + Array, + Matrix, + VariableLengthVector, + VariableSizeMatrix +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/PurePosixPath.java b/packages/core/java/src/main/java/org/itk/wasm/PurePosixPath.java new file mode 100644 index 000000000..b03cb96ab --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/PurePosixPath.java @@ -0,0 +1,48 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; + +public class PurePosixPath { + public Path path; + + public PurePosixPath(Path path) { + this.path = path; + } + public PurePosixPath(String path) { + this.path = Paths.get(path); + } + public PurePosixPath(File path) { + this.path = path.toPath(); + } + + public PurePosixPath getParent() { + return new PurePosixPath(path.getParent()); + } + + @Override + public String toString() { + // TODO: Ensure path string conforms to POSIX requirements. + return path.toString(); + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/TextFile.java b/packages/core/java/src/main/java/org/itk/wasm/TextFile.java new file mode 100644 index 000000000..f501ceeff --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/TextFile.java @@ -0,0 +1,15 @@ +package org.itk.wasm; + +import java.nio.file.Path; + +public class TextFile { + public PurePosixPath path; + + public TextFile(PurePosixPath path) { + this.path = path; + } + + public TextFile(Path path) { + this(new PurePosixPath(path)); + } +} diff --git a/packages/core/java/src/main/java/org/itk/wasm/TextStream.java b/packages/core/java/src/main/java/org/itk/wasm/TextStream.java new file mode 100644 index 000000000..30dc4ed1e --- /dev/null +++ b/packages/core/java/src/main/java/org/itk/wasm/TextStream.java @@ -0,0 +1,28 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +public class TextStream { + public String data; + + public TextStream(String data) { + this.data = data; + } +} diff --git a/packages/core/java/src/main/resources/org/itk/wasm/hello.wat b/packages/core/java/src/main/resources/org/itk/wasm/hello.wat new file mode 100644 index 000000000..1c56c5582 --- /dev/null +++ b/packages/core/java/src/main/resources/org/itk/wasm/hello.wat @@ -0,0 +1,4 @@ +(module + (func $hello (import "" "hello")) + (func (export "run") (call $hello)) +) diff --git a/packages/core/java/src/test/java/org/itk/wasm/TestPipeline.java b/packages/core/java/src/test/java/org/itk/wasm/TestPipeline.java new file mode 100644 index 000000000..bdcdbe0fe --- /dev/null +++ b/packages/core/java/src/test/java/org/itk/wasm/TestPipeline.java @@ -0,0 +1,256 @@ +/*- + * #%L + * Java bindings for itk-wasm. + * %% + * Copyright (C) 2023 ITK developers. + * %% + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * #L% + */ +package org.itk.wasm; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledOnOs; +import org.junit.jupiter.api.condition.OS; + +public class TestPipeline { + + private final Path testDataDir; + private final Path testInputDir; + private final Path testBaselineDir; + + public TestPipeline() { + testDataDir = Paths.get("..", "test", "data").toAbsolutePath(); + testInputDir = testDataDir.resolve("input"); + testBaselineDir = testDataDir.resolve("baseline"); + } + + @Test + public void testStdoutStderr() throws IOException { + Pipeline pipeline = new Pipeline(testInputDir.resolve("stdout-stderr-test.wasi.wasm").toString()); + pipeline.run(new ArrayList<>()); + + // Test re-run + pipeline.run(new ArrayList<>()); + } + + @Test + public void testPipelineInputOutputStreams() throws IOException { + Pipeline pipeline = new Pipeline(testInputDir.resolve("input-output-files-test.wasi.wasm").toString()); + + List> pipelineInputs = new ArrayList<>(); + pipelineInputs.add(new PipelineInput<>(InterfaceType.TextStream, new TextStream("The answer is 42."))); + pipelineInputs.add(new PipelineInput<>(InterfaceType.BinaryStream, new BinaryStream(new byte[]{(byte) 222, (byte) 173, (byte) 190, (byte) 239}))); + + List> pipelineOutputs = new ArrayList<>(); + pipelineOutputs.add(new PipelineOutput<>(InterfaceType.TextStream)); + pipelineOutputs.add(new PipelineOutput<>(InterfaceType.BinaryStream)); + + List args = Arrays.asList( + "--memory-io", + "--input-text-stream", "0", + "--input-binary-stream", "1", + "--output-text-stream", "0", + "--output-binary-stream", "1" + ); + + List> outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + // Test re-run + outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + assert outputs.get(0).type == InterfaceType.TextStream; + assert ((TextStream) outputs.get(0).data).data.equals("The answer is 42."); + + byte[] binaryData = ((BinaryStream) outputs.get(1).data).data; + assert binaryData[0] == (byte) 222; + assert binaryData[1] == (byte) 173; + assert binaryData[2] == (byte) 190; + assert binaryData[3] == (byte) 239; + } + + @EnabledOnOs({OS.LINUX, OS.MAC}) // Skip this test on Windows platform + @Test + public void testPipelineInputOutputFiles() throws IOException { + Pipeline pipeline = new Pipeline(testInputDir.resolve("input-output-files-test.wasi.wasm")); + Path inputTextFile = testInputDir.resolve("input.txt"); + Path inputBinaryFile = testInputDir.resolve("input.bin"); + + File outputTextFile = File.createTempFile("output", ".txt"); + File outputBinaryFile = File.createTempFile("output", ".bin"); + + List> pipelineInputs = new ArrayList<>(); + pipelineInputs.add(new PipelineInput<>(InterfaceType.TextFile, new TextFile(inputTextFile))); + pipelineInputs.add(new PipelineInput<>(InterfaceType.BinaryFile, new BinaryFile(inputBinaryFile))); + + List> pipelineOutputs = new ArrayList<>(); + pipelineOutputs.add(new PipelineOutput<>(InterfaceType.TextFile, new TextFile(outputTextFile.toPath()))); + pipelineOutputs.add(new PipelineOutput<>(InterfaceType.BinaryFile, new BinaryFile(outputBinaryFile.toPath()))); + + List args = Arrays.asList( + "--memory-io", + "--use-files", + "--input-text-file", inputTextFile.toString(), + "--input-binary-file", inputBinaryFile.toString(), + "--output-text-file", outputTextFile.toString(), + "--output-binary-file", outputBinaryFile.toString() + ); + + List> outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + // Test re-run + outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + assert outputs.get(0).type == InterfaceType.TextFile; + PurePosixPath outputPath1 = ((TextFile) outputs.get(0).data).path; + String content1 = IO.readString(outputPath1.toString()); + assert content1.equals("The answer is 42."); + + assert outputs.get(1).type == InterfaceType.BinaryFile; + PurePosixPath outputPath2 = ((BinaryFile) outputs.get(1).data).path; + byte[] content2 = IO.readBytes(outputPath2.toString()); + assert content2[0] == (byte) 222; + assert content2[1] == (byte) 173; + assert content2[2] == (byte) 190; + assert content2[3] == (byte) 239; + } + + /* + @Test + public void testPipelineWriteReadImage() throws IOException { + Pipeline pipeline = new Pipeline(testInputDir.resolve("median-filter-test.wasi.wasm")); + + Path data = testInputDir.resolve("cthead1.png"); + itk.Image itkImage = itk.imread(data, itk.UC); + Map itkImageDict = itk.dict_from_image(itkImage); + Image itkwasmImage = new Image(itkImageDict); + + List> pipelineInputs = new ArrayList<>(); + pipelineInputs.add(new PipelineInput<>(InterfaceTypes.Image, itkwasmImage)); + + List> pipelineOutputs = new ArrayList<>(); + pipelineOutputs.add(new PipelineOutput<>(InterfaceTypes.Image)); + + String[] args = { + "--memory-io", + "0", + "0", + "--radius", "2" + }; + + List> outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + itk.Image outImage = itk.image_from_dict(asdict(outputs.get(0).getData())); + // To be addressed in itk-5.3.1 + outImage.SetRegions(new int[]{256, 256}); + + itk.Image baseline = itk.imread(testBaselineDir.resolve("test_pipeline_write_read_image.png")); + + double difference = np.sum(itk.comparison_image_filter(outImage, baseline)); + assert difference == 0.0; + } + + @Test + public void testPipelineWriteReadMesh() { + Pipeline pipeline = new Pipeline(testInputDir.resolve("mesh-read-write-test.wasi.wasm")); + + Path data = testInputDir.resolve("cow.vtk"); + itk.Mesh itkMesh = itk.meshread(data); + Map itkMeshDict = itk.dict_from_mesh(itkMesh); + Mesh itkwasmMesh = new Mesh(itkMeshDict); + + List> pipelineInputs = new ArrayList<>(); + pipelineInputs.add(new PipelineInput<>(InterfaceTypes.Mesh, itkwasmMesh)); + + List> pipelineOutputs = new ArrayList<>(); + pipelineOutputs.add(new PipelineOutput<>(InterfaceTypes.Mesh)); + + List args = Arrays.asList( + "--memory-io", + "0", + "0" + ); + + List> outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + Map outMeshDict = asdict(outputs.get(0).data); + // Native ITK Python binaries require uint64 + outMeshDict.put("cells", ((int[]) outMeshDict.get("cells")).astype(np.uint64)); + outMeshDict.get("meshType").put("cellComponentType", "uint64"); + itk.Mesh outMesh = itk.mesh_from_dict(outMeshDict); + + assert outMesh.GetNumberOfPoints() == 2903; + assert outMesh.GetNumberOfCells() == 3263; + } + + @Test + public void testPipelineWriteReadPolyData() { + Pipeline pipeline = new Pipeline(testInputDir.resolve("mesh-to-poly-data.wasi.wasm")); + + Path data = testInputDir.resolve("cow.vtk"); + itk.Mesh itkMesh = itk.meshread(data); + Map itkMeshDict = itk.dict_from_mesh(itkMesh); + Mesh itkwasmMesh = new Mesh(itkMeshDict); + + List> pipelineInputs = new ArrayList<>(); + pipelineInputs.add(new PipelineInput<>(InterfaceTypes.Mesh, itkwasmMesh)); + + List> pipelineOutputs = new ArrayList<>(); + pipelineOutputs.add(new PipelineOutput<>(InterfaceTypes.PolyData)); + + List args = Arrays.asList( + "--memory-io", + "0", + "0" + ); + + List> outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + PolyData polydata = (PolyData) outputs.get(0).data; + + pipeline = new Pipeline(testInputDir.resolve("poly-data-to-mesh.wasi.wasm")); + + pipelineInputs = new ArrayList<>(); + pipelineInputs.add(new PipelineInput<>(InterfaceTypes.PolyData, polydata)); + + pipelineOutputs = new ArrayList<>(); + pipelineOutputs.add(new PipelineOutput<>(InterfaceTypes.Mesh)); + + args = Arrays.asList( + "--memory-io", + "0", + "0" + ); + + outputs = pipeline.run(args, pipelineOutputs, pipelineInputs); + + Map outMeshDict = asdict(outputs.get(0).data); + + // native itk python binaries require uint64 + outMeshDict.put("cells", ((int[]) outMeshDict.get("cells")).astype(np.uint64)); + outMeshDict.get("meshType").put("cellComponentType", "uint64"); + assert np.isclose(outMeshDict.get("points")[0], 3.71636); + itk.Mesh outMesh = itk.mesh_from_dict(outMeshDict); + + assert outMesh.GetNumberOfPoints() == 2903; + assert outMesh.GetNumberOfCells() == 3263; + } + */ +} diff --git a/packages/core/python/.gitignore b/packages/core/python/.gitignore new file mode 100644 index 000000000..c18dd8d83 --- /dev/null +++ b/packages/core/python/.gitignore @@ -0,0 +1 @@ +__pycache__/ diff --git a/packages/core/python/itkwasm/itkwasm/transform.py b/packages/core/python/itkwasm/itkwasm/transform.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/packages/core/python/itkwasm/test/test_image.py b/packages/core/python/itkwasm/test/test_image.py index e9a649421..d26087611 100644 --- a/packages/core/python/itkwasm/test/test_image.py +++ b/packages/core/python/itkwasm/test/test_image.py @@ -7,7 +7,7 @@ import numpy as np def test_image(): - data = Path(__file__).absolute().parent / "input" / "cthead1.png" + data = Path(__file__).absolute().parent.parent.parent.parent / "test" / "data" / "input" / "cthead1.png" itk_image = itk.imread(data, itk.UC) itk_image_dict = itk.dict_from_image(itk_image) itkwasm_image = Image(**itk_image_dict) @@ -35,4 +35,4 @@ def test_image_defaults(): assert image.size[1] == 1 assert isinstance(image.metadata, dict) - assert image.data == None \ No newline at end of file + assert image.data == None diff --git a/packages/core/python/itkwasm/test/test_mesh.py b/packages/core/python/itkwasm/test/test_mesh.py index 01c96ae5f..d657b0e51 100644 --- a/packages/core/python/itkwasm/test/test_mesh.py +++ b/packages/core/python/itkwasm/test/test_mesh.py @@ -7,7 +7,7 @@ import numpy as np def test_mesh(): - data = Path(__file__).absolute().parent / "input" / "cow.vtk" + data = Path(__file__).absolute().parent.parent.parent.parent / "test" / "data" / "input" / "cow.vtk" itk_mesh = itk.meshread(data) itk_mesh_dict = itk.dict_from_mesh(itk_mesh) @@ -43,4 +43,4 @@ def test_mesh(): assert itk_mesh_dict["cellBufferSize"] == itk_mesh_roundtrip_dict["cellBufferSize"] assert itk_mesh_dict["numberOfCellPixels"] == itk_mesh_roundtrip_dict["numberOfCellPixels"] - assert np.array_equal(itk_mesh_dict["cellData"], itk_mesh_roundtrip_dict["cellData"]) \ No newline at end of file + assert np.array_equal(itk_mesh_dict["cellData"], itk_mesh_roundtrip_dict["cellData"]) diff --git a/packages/core/python/itkwasm/test/test_pipeline.py b/packages/core/python/itkwasm/test/test_pipeline.py index 666ed5ef7..1470cda54 100644 --- a/packages/core/python/itkwasm/test/test_pipeline.py +++ b/packages/core/python/itkwasm/test/test_pipeline.py @@ -10,8 +10,9 @@ from itkwasm import InterfaceTypes, TextStream, BinaryStream, PipelineInput, PipelineOutput, Pipeline, TextFile, BinaryFile, Image, Mesh -test_input_dir = Path(__file__).resolve().parent / 'input' -test_baseline_dir = Path(__file__).resolve().parent / 'baseline' +test_data_dir = Path(__file__).resolve().parent.parent.parent.parent / 'test' / 'data' +test_input_dir = test_data_dir / 'input' +test_baseline_dir = test_data_dir / 'baseline' def test_stdout_stderr(): diff --git a/packages/core/python/itkwasm/test/baseline/test_pipeline_write_read_image.png b/packages/core/test/data/baseline/test_pipeline_write_read_image.png similarity index 100% rename from packages/core/python/itkwasm/test/baseline/test_pipeline_write_read_image.png rename to packages/core/test/data/baseline/test_pipeline_write_read_image.png diff --git a/packages/core/python/itkwasm/test/input/cow.vtk b/packages/core/test/data/input/cow.vtk similarity index 100% rename from packages/core/python/itkwasm/test/input/cow.vtk rename to packages/core/test/data/input/cow.vtk diff --git a/packages/core/python/itkwasm/test/input/cthead1.png b/packages/core/test/data/input/cthead1.png similarity index 100% rename from packages/core/python/itkwasm/test/input/cthead1.png rename to packages/core/test/data/input/cthead1.png diff --git a/packages/core/python/itkwasm/test/input/input-output-files-test.wasi.wasm b/packages/core/test/data/input/input-output-files-test.wasi.wasm similarity index 100% rename from packages/core/python/itkwasm/test/input/input-output-files-test.wasi.wasm rename to packages/core/test/data/input/input-output-files-test.wasi.wasm diff --git a/packages/core/python/itkwasm/test/input/input.bin b/packages/core/test/data/input/input.bin similarity index 100% rename from packages/core/python/itkwasm/test/input/input.bin rename to packages/core/test/data/input/input.bin diff --git a/packages/core/python/itkwasm/test/input/input.txt b/packages/core/test/data/input/input.txt similarity index 100% rename from packages/core/python/itkwasm/test/input/input.txt rename to packages/core/test/data/input/input.txt diff --git a/packages/core/python/itkwasm/test/input/median-filter-test.wasi.wasm b/packages/core/test/data/input/median-filter-test.wasi.wasm similarity index 100% rename from packages/core/python/itkwasm/test/input/median-filter-test.wasi.wasm rename to packages/core/test/data/input/median-filter-test.wasi.wasm diff --git a/packages/core/python/itkwasm/test/input/mesh-read-write-test.wasi.wasm b/packages/core/test/data/input/mesh-read-write-test.wasi.wasm similarity index 100% rename from packages/core/python/itkwasm/test/input/mesh-read-write-test.wasi.wasm rename to packages/core/test/data/input/mesh-read-write-test.wasi.wasm diff --git a/packages/core/python/itkwasm/test/input/mesh-to-poly-data.wasi.wasm b/packages/core/test/data/input/mesh-to-poly-data.wasi.wasm similarity index 100% rename from packages/core/python/itkwasm/test/input/mesh-to-poly-data.wasi.wasm rename to packages/core/test/data/input/mesh-to-poly-data.wasi.wasm diff --git a/packages/core/python/itkwasm/test/input/poly-data-to-mesh.wasi.wasm b/packages/core/test/data/input/poly-data-to-mesh.wasi.wasm similarity index 100% rename from packages/core/python/itkwasm/test/input/poly-data-to-mesh.wasi.wasm rename to packages/core/test/data/input/poly-data-to-mesh.wasi.wasm diff --git a/packages/core/python/itkwasm/test/input/stdout-stderr-test.wasi.wasm b/packages/core/test/data/input/stdout-stderr-test.wasi.wasm similarity index 100% rename from packages/core/python/itkwasm/test/input/stdout-stderr-test.wasi.wasm rename to packages/core/test/data/input/stdout-stderr-test.wasi.wasm