|
@@ -303,6 +303,21 @@ void Server::registerEndpoints() {
|
|
|
handleDownloadOutput(req, res);
|
|
handleDownloadOutput(req, res);
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ // Download image from URL endpoint (public for CORS-free image handling)
|
|
|
|
|
+ m_httpServer->Get("/api/image/download", [this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ handleDownloadImageFromUrl(req, res);
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ // Image resize endpoint (protected)
|
|
|
|
|
+ m_httpServer->Post("/api/image/resize", withAuth([this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ handleImageResize(req, res);
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
|
|
+ // Image crop endpoint (protected)
|
|
|
|
|
+ m_httpServer->Post("/api/image/crop", withAuth([this](const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ handleImageCrop(req, res);
|
|
|
|
|
+ }));
|
|
|
|
|
+
|
|
|
// Job status endpoint (now protected - require authentication)
|
|
// Job status endpoint (now protected - require authentication)
|
|
|
m_httpServer->Get("/api/queue/job/(.*)", withAuth([this](const httplib::Request& req, httplib::Response& res) {
|
|
m_httpServer->Get("/api/queue/job/(.*)", withAuth([this](const httplib::Request& req, httplib::Response& res) {
|
|
|
handleJobStatus(req, res);
|
|
handleJobStatus(req, res);
|
|
@@ -949,18 +964,25 @@ void Server::handleModelsList(const httplib::Request& req, httplib::Response& re
|
|
|
std::string dateFilter = req.get_param_value("date");
|
|
std::string dateFilter = req.get_param_value("date");
|
|
|
std::string sizeFilter = req.get_param_value("size");
|
|
std::string sizeFilter = req.get_param_value("size");
|
|
|
|
|
|
|
|
- // Pagination parameters
|
|
|
|
|
|
|
+ // Pagination parameters - only apply if limit is explicitly provided
|
|
|
int page = 1;
|
|
int page = 1;
|
|
|
int limit = 50;
|
|
int limit = 50;
|
|
|
|
|
+ bool usePagination = false;
|
|
|
|
|
+
|
|
|
try {
|
|
try {
|
|
|
- if (!req.get_param_value("page").empty()) {
|
|
|
|
|
- page = std::stoi(req.get_param_value("page"));
|
|
|
|
|
- if (page < 1) page = 1;
|
|
|
|
|
- }
|
|
|
|
|
if (!req.get_param_value("limit").empty()) {
|
|
if (!req.get_param_value("limit").empty()) {
|
|
|
limit = std::stoi(req.get_param_value("limit"));
|
|
limit = std::stoi(req.get_param_value("limit"));
|
|
|
- if (limit < 1) limit = 1;
|
|
|
|
|
- if (limit > 200) limit = 200; // Max limit to prevent performance issues
|
|
|
|
|
|
|
+ // Special case: limit<=0 means return all models (no pagination)
|
|
|
|
|
+ if (limit <= 0) {
|
|
|
|
|
+ usePagination = false;
|
|
|
|
|
+ limit = INT_MAX; // Set to very large number to effectively disable pagination
|
|
|
|
|
+ } else {
|
|
|
|
|
+ usePagination = true;
|
|
|
|
|
+ if (!req.get_param_value("page").empty()) {
|
|
|
|
|
+ page = std::stoi(req.get_param_value("page"));
|
|
|
|
|
+ if (page < 1) page = 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
} catch (const std::exception& e) {
|
|
} catch (const std::exception& e) {
|
|
|
sendErrorResponse(res, "Invalid pagination parameters", 400, "INVALID_PAGINATION", requestId);
|
|
sendErrorResponse(res, "Invalid pagination parameters", 400, "INVALID_PAGINATION", requestId);
|
|
@@ -1097,28 +1119,47 @@ void Server::handleModelsList(const httplib::Request& req, httplib::Response& re
|
|
|
});
|
|
});
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Apply pagination
|
|
|
|
|
|
|
+ // Apply pagination only if limit parameter was provided
|
|
|
int totalCount = models.size();
|
|
int totalCount = models.size();
|
|
|
- int totalPages = (totalCount + limit - 1) / limit;
|
|
|
|
|
- int startIndex = (page - 1) * limit;
|
|
|
|
|
- int endIndex = std::min(startIndex + limit, totalCount);
|
|
|
|
|
-
|
|
|
|
|
json paginatedModels = json::array();
|
|
json paginatedModels = json::array();
|
|
|
- for (int i = startIndex; i < endIndex; ++i) {
|
|
|
|
|
- paginatedModels.push_back(models[i]);
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ json paginationInfo = json::object();
|
|
|
|
|
|
|
|
- // Build comprehensive response
|
|
|
|
|
- json response = {
|
|
|
|
|
- {"models", paginatedModels},
|
|
|
|
|
- {"pagination", {
|
|
|
|
|
|
|
+ if (usePagination) {
|
|
|
|
|
+ // Apply pagination
|
|
|
|
|
+ int totalPages = (totalCount + limit - 1) / limit;
|
|
|
|
|
+ int startIndex = (page - 1) * limit;
|
|
|
|
|
+ int endIndex = std::min(startIndex + limit, totalCount);
|
|
|
|
|
+
|
|
|
|
|
+ for (int i = startIndex; i < endIndex; ++i) {
|
|
|
|
|
+ paginatedModels.push_back(models[i]);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ paginationInfo = {
|
|
|
{"page", page},
|
|
{"page", page},
|
|
|
{"limit", limit},
|
|
{"limit", limit},
|
|
|
{"total_count", totalCount},
|
|
{"total_count", totalCount},
|
|
|
{"total_pages", totalPages},
|
|
{"total_pages", totalPages},
|
|
|
{"has_next", page < totalPages},
|
|
{"has_next", page < totalPages},
|
|
|
{"has_prev", page > 1}
|
|
{"has_prev", page > 1}
|
|
|
- }},
|
|
|
|
|
|
|
+ };
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Return all models without pagination
|
|
|
|
|
+ paginatedModels = models;
|
|
|
|
|
+
|
|
|
|
|
+ paginationInfo = {
|
|
|
|
|
+ {"page", 1},
|
|
|
|
|
+ {"limit", totalCount},
|
|
|
|
|
+ {"total_count", totalCount},
|
|
|
|
|
+ {"total_pages", 1},
|
|
|
|
|
+ {"has_next", false},
|
|
|
|
|
+ {"has_prev", false}
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Build comprehensive response
|
|
|
|
|
+ json response = {
|
|
|
|
|
+ {"models", paginatedModels},
|
|
|
|
|
+ {"pagination", paginationInfo},
|
|
|
{"filters_applied", {
|
|
{"filters_applied", {
|
|
|
{"type", typeFilter.empty() ? json(nullptr) : json(typeFilter)},
|
|
{"type", typeFilter.empty() ? json(nullptr) : json(typeFilter)},
|
|
|
{"search", searchQuery.empty() ? json(nullptr) : json(searchQuery)},
|
|
{"search", searchQuery.empty() ? json(nullptr) : json(searchQuery)},
|
|
@@ -1343,6 +1384,7 @@ void Server::handleClearQueue(const httplib::Request& req, httplib::Response& re
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+
|
|
|
void Server::handleDownloadOutput(const httplib::Request& req, httplib::Response& res) {
|
|
void Server::handleDownloadOutput(const httplib::Request& req, httplib::Response& res) {
|
|
|
try {
|
|
try {
|
|
|
// Extract job ID and filename from URL path
|
|
// Extract job ID and filename from URL path
|
|
@@ -1447,6 +1489,367 @@ void Server::handleDownloadOutput(const httplib::Request& req, httplib::Response
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+void Server::handleImageResize(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ std::string requestId = generateRequestId();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse JSON request body
|
|
|
|
|
+ json requestJson = json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ // Validate required fields
|
|
|
|
|
+ if (!requestJson.contains("image") || !requestJson["image"].is_string()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'image' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!requestJson.contains("width") || !requestJson["width"].is_number_integer()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'width' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!requestJson.contains("height") || !requestJson["height"].is_number_integer()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'height' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string imageInput = requestJson["image"];
|
|
|
|
|
+ int targetWidth = requestJson["width"];
|
|
|
|
|
+ int targetHeight = requestJson["height"];
|
|
|
|
|
+
|
|
|
|
|
+ // Validate dimensions
|
|
|
|
|
+ if (targetWidth < 1 || targetWidth > 4096) {
|
|
|
|
|
+ sendErrorResponse(res, "Width must be between 1 and 4096", 400, "INVALID_DIMENSIONS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (targetHeight < 1 || targetHeight > 4096) {
|
|
|
|
|
+ sendErrorResponse(res, "Height must be between 1 and 4096", 400, "INVALID_DIMENSIONS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Load the source image
|
|
|
|
|
+ auto [imageData, sourceWidth, sourceHeight, sourceChannels, success, loadError] = loadImageFromInput(imageInput);
|
|
|
|
|
+
|
|
|
|
|
+ if (!success) {
|
|
|
|
|
+ sendErrorResponse(res, "Failed to load image: " + loadError, 400, "IMAGE_LOAD_ERROR", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Convert image data to stb_image format for processing
|
|
|
|
|
+ int channels = 3; // Force RGB
|
|
|
|
|
+ size_t sourceSize = sourceWidth * sourceHeight * channels;
|
|
|
|
|
+ std::vector<uint8_t> sourcePixels(sourceSize);
|
|
|
|
|
+ std::memcpy(sourcePixels.data(), imageData.data(), std::min(imageData.size(), sourceSize));
|
|
|
|
|
+
|
|
|
|
|
+ // Resize the image using stb_image_resize if available, otherwise use simple scaling
|
|
|
|
|
+ std::vector<uint8_t> resizedPixels(targetWidth * targetHeight * channels);
|
|
|
|
|
+
|
|
|
|
|
+ // Simple nearest-neighbor scaling for now (can be improved with better algorithms)
|
|
|
|
|
+ float xScale = static_cast<float>(sourceWidth) / targetWidth;
|
|
|
|
|
+ float yScale = static_cast<float>(sourceHeight) / targetHeight;
|
|
|
|
|
+
|
|
|
|
|
+ for (int y = 0; y < targetHeight; y++) {
|
|
|
|
|
+ for (int x = 0; x < targetWidth; x++) {
|
|
|
|
|
+ int sourceX = static_cast<int>(x * xScale);
|
|
|
|
|
+ int sourceY = static_cast<int>(y * yScale);
|
|
|
|
|
+
|
|
|
|
|
+ // Clamp to source bounds
|
|
|
|
|
+ sourceX = std::min(sourceX, sourceWidth - 1);
|
|
|
|
|
+ sourceY = std::min(sourceY, sourceHeight - 1);
|
|
|
|
|
+
|
|
|
|
|
+ for (int c = 0; c < channels; c++) {
|
|
|
|
|
+ resizedPixels[(y * targetWidth + x) * channels + c] =
|
|
|
|
|
+ sourcePixels[(sourceY * sourceWidth + sourceX) * channels + c];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Convert resized image to base64
|
|
|
|
|
+ std::string base64Data = Utils::base64Encode(resizedPixels);
|
|
|
|
|
+
|
|
|
|
|
+ // Determine MIME type based on input
|
|
|
|
|
+ std::string mimeType = "image/jpeg"; // default
|
|
|
|
|
+ if (Utils::startsWith(imageInput, "data:image/png")) {
|
|
|
|
|
+ mimeType = "image/png";
|
|
|
|
|
+ } else if (Utils::startsWith(imageInput, "data:image/gif")) {
|
|
|
|
|
+ mimeType = "image/gif";
|
|
|
|
|
+ } else if (Utils::startsWith(imageInput, "data:image/webp")) {
|
|
|
|
|
+ mimeType = "image/webp";
|
|
|
|
|
+ } else if (Utils::startsWith(imageInput, "data:image/bmp")) {
|
|
|
|
|
+ mimeType = "image/bmp";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create data URL format
|
|
|
|
|
+ std::string dataUrl = "data:" + mimeType + ";base64," + base64Data;
|
|
|
|
|
+
|
|
|
|
|
+ // Build response
|
|
|
|
|
+ json response = {
|
|
|
|
|
+ {"success", true},
|
|
|
|
|
+ {"original_width", sourceWidth},
|
|
|
|
|
+ {"original_height", sourceHeight},
|
|
|
|
|
+ {"resized_width", targetWidth},
|
|
|
|
|
+ {"resized_height", targetHeight},
|
|
|
|
|
+ {"mime_type", mimeType},
|
|
|
|
|
+ {"base64_data", dataUrl},
|
|
|
|
|
+ {"file_size_bytes", resizedPixels.size()},
|
|
|
|
|
+ {"request_id", requestId}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ sendJsonResponse(res, response, 200);
|
|
|
|
|
+
|
|
|
|
|
+ std::cout << "Successfully resized image from " << sourceWidth << "x" << sourceHeight
|
|
|
|
|
+ << " to " << targetWidth << "x" << targetHeight
|
|
|
|
|
+ << " (" << resizedPixels.size() << " bytes)" << std::endl;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const json::parse_error& e) {
|
|
|
|
|
+ sendErrorResponse(res, std::string("Invalid JSON: ") + e.what(), 400, "JSON_PARSE_ERROR", requestId);
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ std::cerr << "Exception in handleImageResize: " << e.what() << std::endl;
|
|
|
|
|
+ sendErrorResponse(res, std::string("Failed to resize image: ") + e.what(), 500, "INTERNAL_ERROR", requestId);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Server::handleImageCrop(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ std::string requestId = generateRequestId();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse JSON request body
|
|
|
|
|
+ json requestJson = json::parse(req.body);
|
|
|
|
|
+
|
|
|
|
|
+ // Validate required fields
|
|
|
|
|
+ if (!requestJson.contains("image") || !requestJson["image"].is_string()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'image' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!requestJson.contains("x") || !requestJson["x"].is_number_integer()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'x' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!requestJson.contains("y") || !requestJson["y"].is_number_integer()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'y' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!requestJson.contains("width") || !requestJson["width"].is_number_integer()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'width' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!requestJson.contains("height") || !requestJson["height"].is_number_integer()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing or invalid 'height' field", 400, "INVALID_PARAMETERS", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ std::string imageInput = requestJson["image"];
|
|
|
|
|
+ int cropX = requestJson["x"];
|
|
|
|
|
+ int cropY = requestJson["y"];
|
|
|
|
|
+ int cropWidth = requestJson["width"];
|
|
|
|
|
+ int cropHeight = requestJson["height"];
|
|
|
|
|
+
|
|
|
|
|
+ // Load the source image
|
|
|
|
|
+ auto [imageData, sourceWidth, sourceHeight, sourceChannels, success, loadError] = loadImageFromInput(imageInput);
|
|
|
|
|
+
|
|
|
|
|
+ if (!success) {
|
|
|
|
|
+ sendErrorResponse(res, "Failed to load image: " + loadError, 400, "IMAGE_LOAD_ERROR", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Validate crop dimensions
|
|
|
|
|
+ if (cropX < 0 || cropY < 0) {
|
|
|
|
|
+ sendErrorResponse(res, "Crop coordinates must be non-negative", 400, "INVALID_CROP_AREA", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (cropX + cropWidth > sourceWidth || cropY + cropHeight > sourceHeight) {
|
|
|
|
|
+ sendErrorResponse(res, "Crop area exceeds image dimensions", 400, "INVALID_CROP_AREA", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (cropWidth < 1 || cropHeight < 1) {
|
|
|
|
|
+ sendErrorResponse(res, "Crop width and height must be at least 1", 400, "INVALID_CROP_AREA", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Convert image data to stb_image format for processing
|
|
|
|
|
+ int channels = 3; // Force RGB
|
|
|
|
|
+ size_t sourceSize = sourceWidth * sourceHeight * channels;
|
|
|
|
|
+ std::vector<uint8_t> sourcePixels(sourceSize);
|
|
|
|
|
+ std::memcpy(sourcePixels.data(), imageData.data(), std::min(imageData.size(), sourceSize));
|
|
|
|
|
+
|
|
|
|
|
+ // Crop the image
|
|
|
|
|
+ std::vector<uint8_t> croppedPixels(cropWidth * cropHeight * channels);
|
|
|
|
|
+
|
|
|
|
|
+ for (int y = 0; y < cropHeight; y++) {
|
|
|
|
|
+ for (int x = 0; x < cropWidth; x++) {
|
|
|
|
|
+ int sourceX = cropX + x;
|
|
|
|
|
+ int sourceY = cropY + y;
|
|
|
|
|
+
|
|
|
|
|
+ for (int c = 0; c < channels; c++) {
|
|
|
|
|
+ croppedPixels[(y * cropWidth + x) * channels + c] =
|
|
|
|
|
+ sourcePixels[(sourceY * sourceWidth + sourceX) * channels + c];
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Convert cropped image to base64
|
|
|
|
|
+ std::string base64Data = Utils::base64Encode(croppedPixels);
|
|
|
|
|
+
|
|
|
|
|
+ // Determine MIME type based on input
|
|
|
|
|
+ std::string mimeType = "image/jpeg"; // default
|
|
|
|
|
+ if (Utils::startsWith(imageInput, "data:image/png")) {
|
|
|
|
|
+ mimeType = "image/png";
|
|
|
|
|
+ } else if (Utils::startsWith(imageInput, "data:image/gif")) {
|
|
|
|
|
+ mimeType = "image/gif";
|
|
|
|
|
+ } else if (Utils::startsWith(imageInput, "data:image/webp")) {
|
|
|
|
|
+ mimeType = "image/webp";
|
|
|
|
|
+ } else if (Utils::startsWith(imageInput, "data:image/bmp")) {
|
|
|
|
|
+ mimeType = "image/bmp";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create data URL format
|
|
|
|
|
+ std::string dataUrl = "data:" + mimeType + ";base64," + base64Data;
|
|
|
|
|
+
|
|
|
|
|
+ // Build response
|
|
|
|
|
+ json response = {
|
|
|
|
|
+ {"success", true},
|
|
|
|
|
+ {"original_width", sourceWidth},
|
|
|
|
|
+ {"original_height", sourceHeight},
|
|
|
|
|
+ {"crop_x", cropX},
|
|
|
|
|
+ {"crop_y", cropY},
|
|
|
|
|
+ {"cropped_width", cropWidth},
|
|
|
|
|
+ {"cropped_height", cropHeight},
|
|
|
|
|
+ {"mime_type", mimeType},
|
|
|
|
|
+ {"base64_data", dataUrl},
|
|
|
|
|
+ {"file_size_bytes", croppedPixels.size()},
|
|
|
|
|
+ {"request_id", requestId}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ sendJsonResponse(res, response, 200);
|
|
|
|
|
+
|
|
|
|
|
+ std::cout << "Successfully cropped image from " << sourceWidth << "x" << sourceHeight
|
|
|
|
|
+ << " to " << cropWidth << "x" << cropHeight
|
|
|
|
|
+ << " at (" << cropX << "," << cropY << ")"
|
|
|
|
|
+ << " (" << croppedPixels.size() << " bytes)" << std::endl;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const json::parse_error& e) {
|
|
|
|
|
+ sendErrorResponse(res, std::string("Invalid JSON: ") + e.what(), 400, "JSON_PARSE_ERROR", requestId);
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ std::cerr << "Exception in handleImageCrop: " << e.what() << std::endl;
|
|
|
|
|
+ sendErrorResponse(res, std::string("Failed to crop image: ") + e.what(), 500, "INTERNAL_ERROR", requestId);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+void Server::handleDownloadImageFromUrl(const httplib::Request& req, httplib::Response& res) {
|
|
|
|
|
+ std::string requestId = generateRequestId();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ // Parse query parameters
|
|
|
|
|
+ std::string imageUrl = req.get_param_value("url");
|
|
|
|
|
+
|
|
|
|
|
+ if (imageUrl.empty()) {
|
|
|
|
|
+ sendErrorResponse(res, "Missing 'url' parameter", 400, "MISSING_URL", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Basic URL format validation
|
|
|
|
|
+ if (!Utils::startsWith(imageUrl, "http://") && !Utils::startsWith(imageUrl, "https://")) {
|
|
|
|
|
+ sendErrorResponse(res, "Invalid URL format. URL must start with http:// or https://", 400, "INVALID_URL_FORMAT", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Extract filename from URL for content type detection
|
|
|
|
|
+ std::string filename = imageUrl;
|
|
|
|
|
+ size_t lastSlash = imageUrl.find_last_of('/');
|
|
|
|
|
+ if (lastSlash != std::string::npos) {
|
|
|
|
|
+ filename = imageUrl.substr(lastSlash + 1);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Remove query parameters and fragments
|
|
|
|
|
+ size_t questionMark = filename.find('?');
|
|
|
|
|
+ if (questionMark != std::string::npos) {
|
|
|
|
|
+ filename = filename.substr(0, questionMark);
|
|
|
|
|
+ }
|
|
|
|
|
+ size_t hashMark = filename.find('#');
|
|
|
|
|
+ if (hashMark != std::string::npos) {
|
|
|
|
|
+ filename = filename.substr(0, hashMark);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Check if URL has image extension
|
|
|
|
|
+ std::string extension;
|
|
|
|
|
+ size_t lastDot = filename.find_last_of('.');
|
|
|
|
|
+ if (lastDot != std::string::npos) {
|
|
|
|
|
+ extension = filename.substr(lastDot + 1);
|
|
|
|
|
+ std::transform(extension.begin(), extension.end(), extension.begin(), ::tolower);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Validate image extension
|
|
|
|
|
+ const std::vector<std::string> validExtensions = {"jpg", "jpeg", "png", "gif", "webp", "bmp"};
|
|
|
|
|
+ if (extension.empty() || std::find(validExtensions.begin(), validExtensions.end(), extension) == validExtensions.end()) {
|
|
|
|
|
+ sendErrorResponse(res, "URL must point to an image file with a valid extension: " +
|
|
|
|
|
+ std::accumulate(validExtensions.begin(), validExtensions.end(), std::string(),
|
|
|
|
|
+ [](const std::string& a, const std::string& b) {
|
|
|
|
|
+ return a.empty() ? b : a + ", " + b;
|
|
|
|
|
+ }), 400, "INVALID_IMAGE_EXTENSION", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Load image using existing loadImageFromInput function
|
|
|
|
|
+ auto [imageData, width, height, channels, success, error] = loadImageFromInput(imageUrl);
|
|
|
|
|
+
|
|
|
|
|
+ if (!success) {
|
|
|
|
|
+ sendErrorResponse(res, "Failed to download image from URL: " + error, 400, "IMAGE_DOWNLOAD_FAILED", requestId);
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Convert image data to base64
|
|
|
|
|
+ std::string base64Data = Utils::base64Encode(imageData);
|
|
|
|
|
+
|
|
|
|
|
+ // Determine MIME type based on extension
|
|
|
|
|
+ std::string mimeType = "image/jpeg"; // default
|
|
|
|
|
+ if (extension == "png") {
|
|
|
|
|
+ mimeType = "image/png";
|
|
|
|
|
+ } else if (extension == "gif") {
|
|
|
|
|
+ mimeType = "image/gif";
|
|
|
|
|
+ } else if (extension == "webp") {
|
|
|
|
|
+ mimeType = "image/webp";
|
|
|
|
|
+ } else if (extension == "bmp") {
|
|
|
|
|
+ mimeType = "image/bmp";
|
|
|
|
|
+ } else if (extension == "jpg" || extension == "jpeg") {
|
|
|
|
|
+ mimeType = "image/jpeg";
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Create data URL format
|
|
|
|
|
+ std::string dataUrl = "data:" + mimeType + ";base64," + base64Data;
|
|
|
|
|
+
|
|
|
|
|
+ // Build response
|
|
|
|
|
+ json response = {
|
|
|
|
|
+ {"success", true},
|
|
|
|
|
+ {"url", imageUrl},
|
|
|
|
|
+ {"filename", filename},
|
|
|
|
|
+ {"width", width},
|
|
|
|
|
+ {"height", height},
|
|
|
|
|
+ {"channels", channels},
|
|
|
|
|
+ {"mime_type", mimeType},
|
|
|
|
|
+ {"base64_data", dataUrl},
|
|
|
|
|
+ {"file_size_bytes", imageData.size()},
|
|
|
|
|
+ {"request_id", requestId}
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ sendJsonResponse(res, response, 200);
|
|
|
|
|
+
|
|
|
|
|
+ std::cout << "Successfully downloaded and encoded image from URL: " << imageUrl
|
|
|
|
|
+ << " (" << width << "x" << height << ", " << imageData.size() << " bytes)" << std::endl;
|
|
|
|
|
+
|
|
|
|
|
+ } catch (const json::parse_error& e) {
|
|
|
|
|
+ sendErrorResponse(res, std::string("Invalid JSON: ") + e.what(), 400, "JSON_PARSE_ERROR", requestId);
|
|
|
|
|
+ } catch (const std::exception& e) {
|
|
|
|
|
+ std::cerr << "Exception in handleDownloadImageFromUrl: " << e.what() << std::endl;
|
|
|
|
|
+ sendErrorResponse(res, std::string("Failed to download image from URL: ") + e.what(), 500, "INTERNAL_ERROR", requestId);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
void Server::sendJsonResponse(httplib::Response& res, const nlohmann::json& json, int status_code) {
|
|
void Server::sendJsonResponse(httplib::Response& res, const nlohmann::json& json, int status_code) {
|
|
|
res.set_header("Content-Type", "application/json");
|
|
res.set_header("Content-Type", "application/json");
|
|
|
res.status = status_code;
|
|
res.status = status_code;
|
|
@@ -4488,7 +4891,17 @@ void Server::serverThreadFunction(const std::string& host, int port) {
|
|
|
if (test_socket >= 0) {
|
|
if (test_socket >= 0) {
|
|
|
// Set SO_REUSEADDR to avoid TIME_WAIT issues
|
|
// Set SO_REUSEADDR to avoid TIME_WAIT issues
|
|
|
int opt = 1;
|
|
int opt = 1;
|
|
|
- setsockopt(test_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
|
|
|
|
|
|
|
+ if (setsockopt(test_socket, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)) < 0) {
|
|
|
|
|
+ std::cerr << "Warning: Failed to set SO_REUSEADDR on test socket: " << strerror(errno) << std::endl;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // Also set SO_REUSEPORT if available (for better concurrent binding handling)
|
|
|
|
|
+ #ifdef SO_REUSEPORT
|
|
|
|
|
+ int reuseport = 1;
|
|
|
|
|
+ if (setsockopt(test_socket, SOL_SOCKET, SO_REUSEPORT, &reuseport, sizeof(reuseport)) < 0) {
|
|
|
|
|
+ std::cerr << "Warning: Failed to set SO_REUSEPORT on test socket: " << strerror(errno) << std::endl;
|
|
|
|
|
+ }
|
|
|
|
|
+ #endif
|
|
|
|
|
|
|
|
struct sockaddr_in addr;
|
|
struct sockaddr_in addr;
|
|
|
addr.sin_family = AF_INET;
|
|
addr.sin_family = AF_INET;
|
|
@@ -4499,7 +4912,15 @@ void Server::serverThreadFunction(const std::string& host, int port) {
|
|
|
if (bind(test_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
if (bind(test_socket, (struct sockaddr*)&addr, sizeof(addr)) < 0) {
|
|
|
close(test_socket);
|
|
close(test_socket);
|
|
|
std::cerr << "ERROR: Port " << port << " is already in use! Cannot start server." << std::endl;
|
|
std::cerr << "ERROR: Port " << port << " is already in use! Cannot start server." << std::endl;
|
|
|
- std::cerr << "Please stop the existing instance or use a different port." << std::endl;
|
|
|
|
|
|
|
+ std::cerr << "This could be due to:" << std::endl;
|
|
|
|
|
+ std::cerr << "1. Another instance is already running on this port" << std::endl;
|
|
|
|
|
+ std::cerr << "2. A previous instance crashed and the socket is in TIME_WAIT state" << std::endl;
|
|
|
|
|
+ std::cerr << "3. The port is being used by another application" << std::endl;
|
|
|
|
|
+ std::cerr << std::endl;
|
|
|
|
|
+ std::cerr << "Solutions:" << std::endl;
|
|
|
|
|
+ std::cerr << "- Wait 30-60 seconds for TIME_WAIT to expire (if server crashed)" << std::endl;
|
|
|
|
|
+ std::cerr << "- Kill any existing processes: sudo lsof -ti:" << port << " | xargs kill -9" << std::endl;
|
|
|
|
|
+ std::cerr << "- Use a different port with -p <port>" << std::endl;
|
|
|
m_isRunning.store(false);
|
|
m_isRunning.store(false);
|
|
|
m_startupFailed.store(true);
|
|
m_startupFailed.store(true);
|
|
|
return;
|
|
return;
|