diff --git a/Package.resolved b/Package.resolved index 95810d7..e392374 100644 --- a/Package.resolved +++ b/Package.resolved @@ -149,8 +149,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/valeriyvan/swift-geometrize.git", "state" : { - "revision" : "b874832b6824922f877664a78fccee7a7d258705", - "version" : "1.1.1" + "branch" : "feature/asynciterator", + "revision" : "c82b7e104ce6ed94f55530a6ef1351ae4ad23ab9" } }, { @@ -284,8 +284,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/nerzh/telegram-vapor-bot", "state" : { - "revision" : "0c3469a60bee8170765ab9f108263e76cd065441", - "version" : "2.4.3" + "revision" : "1e7a08260f354c5e131bac8722494f2f61d786c6", + "version" : "2.5.0" } }, { @@ -293,8 +293,8 @@ "kind" : "remoteSourceControl", "location" : "https://github.com/vapor/vapor.git", "state" : { - "revision" : "67fe736c37b0ad958b9d248f010cff6c1baa5c3a", - "version" : "4.89.3" + "revision" : "6db3d917b5ce5024a84eb265ef65691383305d70", + "version" : "4.90.0" } }, { diff --git a/Package.swift b/Package.swift index b7d4b07..f4ada8b 100644 --- a/Package.swift +++ b/Package.swift @@ -7,11 +7,17 @@ let package = Package( .macOS(.v12) ], dependencies: [ - .package(url: "https://github.com/vapor/vapor.git", from: "4.76.0"), + .package(url: "https://github.com/vapor/vapor.git", from: "4.90.0"), .package(url: "https://github.com/vapor/leaf", from: "4.2.4"), .package(url: "https://github.com/vapor/leaf-kit", from: "1.10.2"), - .package(url: "https://github.com/nerzh/telegram-vapor-bot", from: "2.4.3"), - .package(url: "https://github.com/valeriyvan/swift-geometrize.git", from: "1.1.1"), + .package(url: "https://github.com/nerzh/telegram-vapor-bot", from: "2.5.0"), + // As remainder how to add local package as dependency: + // https://stackoverflow.com/questions/49819552/swift-4-local-dependencies-with-swift-package-manager + // .package(name: "MyPackage", path: "/local/path/to/package"), + // .package(path: "../Modules/MySwiftLib"), + // .package(url: "file:///path/to/MySwiftLib", from: "1.0.0"), + // .package(url: "../swift-geometrize/", branch: "feature/asynciterator"), + .package(url: "https://github.com/valeriyvan/swift-geometrize.git", branch: "feature/asynciterator"), .package(url: "https://github.com/valeriyvan/jpeg.git", from: "1.0.2"), .package(url: "https://github.com/kelvin13/swift-png.git", from: "4.0.2"), .package(url: "https://github.com/awslabs/aws-sdk-swift", exact: "0.17.0") diff --git a/Sources/App/DefaultHandlers.swift b/Sources/App/DefaultHandlers.swift index 8051894..869b754 100644 --- a/Sources/App/DefaultHandlers.swift +++ b/Sources/App/DefaultHandlers.swift @@ -95,7 +95,7 @@ final class DefaultBotHandlers { chatId: .chat(message.chat.id), messageThreadId: nil, // TODO: ??? text: "How would you like your image to be geometrized?", - replyToMessageId: message.messageId, + replyParameters: TGReplyParameters(messageId: message.messageId), replyMarkup: .replyKeyboardMarkup(keyboard) ) try await bot.sendMessage(params: params) @@ -143,7 +143,7 @@ final class DefaultBotHandlers { chatId: .chat(message.chat.id), messageThreadId: nil, // TODO: ??? text: "What's stroke width?", - replyToMessageId: message.messageId, + replyParameters: TGReplyParameters(messageId: message.messageId), replyMarkup: .replyKeyboardMarkup(keyboard) ) try await bot.sendMessage(params: params) @@ -171,7 +171,7 @@ final class DefaultBotHandlers { chatId: .chat(message.chat.id), messageThreadId: nil, // TODO: ??? text: "How many shapes?", - replyToMessageId: message.messageId, + replyParameters: TGReplyParameters(messageId: message.messageId), replyMarkup: .replyKeyboardMarkup(keyboard) ) try await bot.sendMessage(params: params) @@ -205,7 +205,7 @@ final class DefaultBotHandlers { chatId: .chat(message.chat.id), messageThreadId: nil, // TODO: ??? text: "How many shapes?", - replyToMessageId: message.messageId, + replyParameters: TGReplyParameters(messageId: message.messageId), replyMarkup: .replyKeyboardMarkup(keyboard) ) try await bot.sendMessage(params: params) @@ -236,16 +236,17 @@ final class DefaultBotHandlers { " Will post here \(iterations - 1) intermediary geometrizing results and then final one." : "" ), - replyToMessageId: message.messageId + replyParameters: TGReplyParameters(messageId: message.messageId) ) try await bot.sendMessage(params: params) - let svgSequence = try await Geometrizer.geometrize( + let svgSequence = try await SVGAsyncGeometrizer.geometrize( bitmap: imageData.bitmap, shapeTypes: types, strokeWidth: strokeWidths[userId] ?? 1, iterations: iterations, - shapesPerIteration: shapesPerIteration + shapesPerIteration: shapesPerIteration, + iterationOptions: .completeSVGEachIteration ) var shapesCounter = shapesPerIteration var iteration = 0 @@ -279,7 +280,7 @@ final class DefaultBotHandlers { document: .file(file), thumbnail: .file(thumbnail), caption: iterations > 1 ? "\(iteration + 1)/\(iterations)" : nil, - replyToMessageId: imageData.messageId + replyParameters: TGReplyParameters(messageId: message.messageId) ) ) shapesCounter += shapesPerIteration @@ -327,7 +328,7 @@ final class DefaultBotHandlers { chatId: .chat(message.chat.id), messageThreadId: nil, // TODO: ??? text: "Try send an image...", - replyToMessageId: message.messageId + replyParameters: TGReplyParameters(messageId: message.messageId) ) state[userId] = .waitImageFromUser try await connection.bot.sendMessage(params: params) diff --git a/Sources/App/Geometrizer.swift b/Sources/App/Geometrizer.swift deleted file mode 100644 index 4c9bf92..0000000 --- a/Sources/App/Geometrizer.swift +++ /dev/null @@ -1,164 +0,0 @@ -import Foundation -import Geometrize -import JPEG -import PNG - -enum Geometrizer { - - // Returns SVGAsyncSequence which produces intermediate geometrizing results - // which are SVG strings + thumbnails. The last sequence element is final result. - static func geometrize( - bitmap: Bitmap, - shapeTypes: [Shape.Type], - strokeWidth: Int, - iterations: Int, - shapesPerIteration: Int - ) async throws -> SVGAsyncSequence { - SVGAsyncSequence( - bitmap: bitmap, - shapeTypes: shapeTypes, - strokeWidth: strokeWidth, - iterations: iterations, - shapesPerIteration: shapesPerIteration - ) - } - -} - -struct SVGIterator: AsyncIteratorProtocol { - private let originalPhotoWidth: Int - private let originalPhotoHeight: Int - private let shapeTypes: [Shape.Type] - private let iterations: Int - private let shapesPerIteration: Int - - private let width, height: Int - - private var iterationCounter: Int - - private var shapeData: [ShapeResult] - - private let runnerOptions: ImageRunnerOptions - private var runner: ImageRunner - - // Counts attempts to add shapes. Not all attempts to add shape result in adding a shape. - private var stepCounter: Int - - init( - bitmap: Bitmap, - downscaleImageToMaxSize downscaleSize: Int = 500, - shapeTypes: [Shape.Type], - strokeWidth: Int, - iterations: Int, - shapesPerIteration: Int - ) { - self.shapeTypes = shapeTypes - self.iterations = iterations - self.shapesPerIteration = shapesPerIteration - - var targetBitmap = bitmap - originalPhotoWidth = bitmap.width - originalPhotoHeight = bitmap.height - let maxSize = max(originalPhotoWidth, originalPhotoHeight) - if maxSize > downscaleSize { - targetBitmap = bitmap.downsample(factor: maxSize / downscaleSize) - } - - //targetBitmap.transpose() - //targetBitmap.reflectVertically() - - width = targetBitmap.width - height = targetBitmap.height - - iterationCounter = 0 - - stepCounter = 0 - - runnerOptions = ImageRunnerOptions( - shapeTypes: shapeTypes, - strokeWidth: strokeWidth, - alpha: 128, - shapeCount: 500, // ? - maxShapeMutations: 100, - seed: 9001, // ! - maxThreads: 1, - shapeBounds: ImageRunnerShapeBoundsOptions( - enabled: false, - xMinPercent: 0, yMinPercent: 0, xMaxPercent: 100, yMaxPercent: 100 - ) - ) - - runner = ImageRunner(targetBitmap: targetBitmap) - - shapeData = [] - - // Hack to add a single background rectangle as the initial shape - shapeData.append( - ShapeResult( - score: 0, - color: targetBitmap.averageColor(), - shape: Rectangle(canvasWidth: targetBitmap.width, height: targetBitmap.height) - ) - ) - } - - mutating func next() async throws -> GeometrizingResult? { - guard iterationCounter < iterations else { return nil } - var stepShapeData: [ShapeResult] = [] - while stepShapeData.count < shapesPerIteration { - print("Step \(stepCounter)", terminator: "") - let stepResult = runner.step( - options: runnerOptions, - shapeCreator: nil, - energyFunction: defaultEnergyFunction, - addShapePrecondition: defaultAddShapePrecondition - ) - if let stepResult { - print(", \(stepResult.shape.description) added.", terminator: "") - stepShapeData.append(stepResult) - } else { - print(", no shapes added.", terminator: "") - } - print(" Total count of shapes \(shapeData.count + stepShapeData.count).") - stepCounter += 1 - } - - shapeData.append(contentsOf: stepShapeData) - iterationCounter += 1 - - var svg = SVGExporter().export(data: shapeData, width: width, height: height) - - // Fix SVG to keep original image size - let range = svg.range(of: "width=")!.lowerBound ..< svg.range(of: "viewBox=")!.lowerBound - svg.replaceSubrange(range.relative(to: svg), with: " width=\"\(originalPhotoWidth)\" height=\"\(originalPhotoHeight)\" ") - - print("Iteration \(iterationCounter) complete, \(stepShapeData.count) shapes in iteration, \(shapeData.count) shapes in total.") - return GeometrizingResult(svg: svg, thumbnail: runner.currentBitmap) - } - -} - -struct GeometrizingResult { - let svg: String - let thumbnail: Bitmap -} - -struct SVGAsyncSequence: AsyncSequence { - typealias Element = GeometrizingResult - - let bitmap: Bitmap - let shapeTypes: [Shape.Type] - let strokeWidth: Int - let iterations: Int - let shapesPerIteration: Int - - func makeAsyncIterator() -> SVGIterator { - SVGIterator( - bitmap: bitmap, - shapeTypes: shapeTypes, - strokeWidth: strokeWidth, - iterations: iterations, - shapesPerIteration: shapesPerIteration - ) - } -} diff --git a/Sources/App/routes.swift b/Sources/App/routes.swift index 7034cba..1b1d853 100644 --- a/Sources/App/routes.swift +++ b/Sources/App/routes.swift @@ -2,11 +2,13 @@ import Vapor import Geometrize import Leaf -var cache: [String: (date: Date, iterator: SVGIterator)] = [:] +var cache: [String: (date: Date, iterator: SVGAsyncIterator)] = [:] -var iterators: [UUID: (date: Date, iterator: SVGIterator)] = [:] +var iterators: [UUID: (date: Date, iterator: SVGAsyncIterator)] = [:] func routes(_ app: Application) throws { + let updateMarker = "\n" + app.get { req in cleanup() return req.leaf.render("index") @@ -47,12 +49,13 @@ func routes(_ app: Application) throws { throw "Cannot process file \(input.file.filename)" } - let svgSequence: SVGAsyncSequence = try await Geometrizer.geometrize( + let svgSequence: SVGAsyncSequence = try await SVGAsyncGeometrizer.geometrize( bitmap: bitmap, shapeTypes: [selectedShape], strokeWidth: 1, iterations: steps, - shapesPerIteration: shapeCount / steps + shapesPerIteration: shapeCount / steps, + iterationOptions: .completeSVGEachIteration ) var asyncIterator = svgSequence.makeAsyncIterator() cache[id] = (date: Date(), iterator: asyncIterator) @@ -112,12 +115,13 @@ func routes(_ app: Application) throws { throw "Cannot process file \(input.file.filename)" } - let svgSequence: SVGAsyncSequence = try await Geometrizer.geometrize( + let svgSequence: SVGAsyncSequence = try await SVGAsyncGeometrizer.geometrize( bitmap: bitmap, shapeTypes: [selectedShape], strokeWidth: 1, iterations: shapeCount, - shapesPerIteration: 1 + shapesPerIteration: 1, + iterationOptions: .completeSVGFirstIterationThenDeltas(updateMarker: updateMarker) ) let asyncIterator = svgSequence.makeAsyncIterator() let uuid = UUID() @@ -135,10 +139,20 @@ func routes(_ app: Application) throws { try? await ws.close(code: .unacceptableData) return } + var firstSVG: String? = nil + var svgAdOns: String = "" + // for try await result in svgSequence { TODO: do this! while let result = try? await iterator.next() { - let svgLines = result.svg.components(separatedBy: .newlines) - let svg = svgLines.dropFirst(2).joined(separator: "\n") - try? await ws.send(svg) + if let firstSVG { + svgAdOns += result.svg + let fullSVG = firstSVG.replacingOccurrences(of: updateMarker, with: svgAdOns) + try? await ws.send(fullSVG) + } else { + let svgLines = result.svg.components(separatedBy: .newlines) + let firstSVG_ = svgLines.dropFirst(2).joined(separator: "\n") + try? await ws.send(firstSVG_) + firstSVG = firstSVG_ + } } try? await ws.close() }