diff --git a/Userland/Utilities/CMakeLists.txt b/Userland/Utilities/CMakeLists.txt index b9f1ebe0a0..75955fe49d 100644 --- a/Userland/Utilities/CMakeLists.txt +++ b/Userland/Utilities/CMakeLists.txt @@ -8,7 +8,7 @@ list(APPEND REQUIRED_TARGETS ) list(APPEND RECOMMENDED_TARGETS adjtime aplay abench asctl bt checksum chres cksum copy fortune gunzip gzip init install keymap lsirq lsof lspci lzcat man mknod mktemp - nc netstat notify ntpquery open passwd pls printf pro shot strings tar tt unzip wallpaper xzcat zip + nc netstat notify ntpquery open passwd pixelflut pls printf pro shot strings tar tt unzip wallpaper xzcat zip ) # FIXME: Support specifying component dependencies for utilities (e.g. WebSocket for telws) @@ -122,6 +122,7 @@ target_link_libraries(open PRIVATE LibDesktop LibFileSystem) target_link_libraries(passwd PRIVATE LibCrypt) target_link_libraries(paste PRIVATE LibGUI) target_link_libraries(pgrep PRIVATE LibRegex) +target_link_libraries(pixelflut PRIVATE LibImageDecoderClient LibIPC LibGfx) target_link_libraries(pkill PRIVATE LibRegex) target_link_libraries(pls PRIVATE LibCrypt) target_link_libraries(pro PRIVATE LibFileSystem LibProtocol LibHTTP) diff --git a/Userland/Utilities/pixelflut.cpp b/Userland/Utilities/pixelflut.cpp new file mode 100644 index 0000000000..a596ba6173 --- /dev/null +++ b/Userland/Utilities/pixelflut.cpp @@ -0,0 +1,164 @@ +/* + * Copyright (c) 2023, kleines Filmröllchen . + * + * SPDX-License-Identifier: BSD-2-Clause + */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +constexpr StringView get_command = "SIZE\n"sv; + +// Client for the Pixelflut protocol +// https://github.com/defnull/pixelflut#pixelflut-protocol +class Client : public RefCounted { +public: + static ErrorOr> create(StringView image_path, StringView server, size_t x, size_t y); + + ErrorOr run(); + +private: + Client(NonnullOwnPtr, NonnullRefPtr, Gfx::IntSize, Gfx::IntPoint); + + ErrorOr send_current_pixel(); + void next_pixel(); + + NonnullOwnPtr m_socket; + + NonnullRefPtr const m_image; + Gfx::IntSize const m_canvas_size; + Gfx::IntPoint const m_image_offset; + + Gfx::IntPoint m_current_point { 0, 0 }; +}; + +ErrorOr> Client::create(StringView image_path, StringView server, size_t x, size_t y) +{ + // Extract hostname and port and connect to server. + auto parts = server.split_view(':'); + auto maybe_port = parts.take_last().to_uint(); + if (!maybe_port.has_value()) + return Error::from_string_view("Invalid port number"sv); + + auto port = maybe_port.release_value(); + auto hostname = TRY(String::join(':', parts)); + + auto socket = TRY(Core::BufferedTCPSocket::create(TRY(Core::TCPSocket::connect(hostname.to_deprecated_string(), port)))); + + // Ask the server for the canvas size. + TRY(socket->write_until_depleted(get_command.bytes())); + auto buffer = TRY(ByteBuffer::create_zeroed(KiB)); + auto size_line = TRY(socket->read_line(buffer)); + if (!size_line.starts_with("SIZE"sv)) + return Error::from_string_view("Server didn't return size correctly"sv); + + auto size_parts = size_line.split_view(' '); + auto maybe_width = size_parts[1].to_uint(); + auto maybe_height = size_parts[2].to_uint(); + if (!maybe_width.has_value() || !maybe_height.has_value()) + return Error::from_string_view("Width or height invalid"sv); + + auto width = maybe_width.release_value(); + auto height = maybe_height.release_value(); + Gfx::IntSize canvas_size = { width, height }; + + // Read input image. + auto image_file = TRY(Core::File::open(image_path, Core::File::OpenMode::Read)); + auto image_decoder = TRY(ImageDecoderClient::Client::try_create()); + ScopeGuard guard = [&] { image_decoder->shutdown(); }; + + auto byte_buffer = TRY(image_file->read_until_eof(16 * KiB)); + auto maybe_image = image_decoder->decode_image(byte_buffer); + + if (!maybe_image.has_value()) + return Error::from_string_view("Image could not be read"sv); + + auto image = maybe_image->frames.take_first().bitmap.release_nonnull(); + + // Make sure to not draw out of bounds; some servers will disconnect us for that! + if (image->width() > canvas_size.width()) { + auto fitting_scale = static_cast(canvas_size.width()) / image->width(); + image = TRY(image->scaled(fitting_scale, fitting_scale)); + } + + return TRY(adopt_nonnull_ref_or_enomem(new (nothrow) Client(move(socket), move(image), canvas_size, Gfx::IntPoint { x, y }))); +} + +Client::Client(NonnullOwnPtr socket, NonnullRefPtr image, Gfx::IntSize canvas_size, Gfx::IntPoint image_offset) + : m_socket(move(socket)) + , m_image(move(image)) + , m_canvas_size(canvas_size) + , m_image_offset(image_offset) +{ + outln("Connected to server, image {}, canvas size {}", m_image, m_canvas_size); +} + +ErrorOr Client::run() +{ + while (true) { + TRY(send_current_pixel()); + next_pixel(); + } +} + +ErrorOr Client::send_current_pixel() +{ + auto color = m_image->get_pixel(m_current_point); + if (color.alpha() == 0) + return {}; + auto hex = color.to_deprecated_string(); + // Pixelflut requires hex colors without leading hash. + auto hex_without_hash = hex.substring(1); + + // PX + TRY(m_socket->write_formatted("PX {} {} {}\n", m_current_point.x() + m_image_offset.x(), m_current_point.y() + m_image_offset.y(), hex_without_hash)); + return {}; +} + +void Client::next_pixel() +{ + m_current_point.set_x(m_current_point.x() + 1); + if (m_current_point.x() >= m_image->width()) { + m_current_point.set_x(0); + m_current_point.set_y(m_current_point.y() + 1); + if (m_current_point.y() >= m_image->height()) { + m_current_point.set_x(0); + m_current_point.set_y(0); + } + } +} + +ErrorOr serenity_main(Main::Arguments arguments) +{ + Core::EventLoop loop; + + StringView image_path; + size_t x; + size_t y; + StringView server; + + Core::ArgsParser args_parser; + args_parser.add_option(image_path, "Image to send to server", "image", 'i', "IMAGE"); + args_parser.add_option(x, "Target x coordinate of the image on the server", "x", 'x', "X"); + args_parser.add_option(y, "Target y coordinate of the image on the server", "y", 'y', "Y"); + args_parser.add_positional_argument(server, "Pixelflut server (hostname:port)", "server"); + args_parser.parse(arguments); + + if (image_path.is_empty()) { + warnln("Error: -i argument is required"); + return 1; + } + + auto client = TRY(Client::create(image_path, server, x, y)); + + TRY(client->run()); + + return 0; +}