diff --git a/pom.xml b/pom.xml index c3c843d..2b04043 100644 --- a/pom.xml +++ b/pom.xml @@ -1,129 +1,139 @@ - 4.0.0 - - org.springframework.boot - spring-boot-starter-parent - 3.1.5 - - - com.appdev - all-in - 0.0.1-SNAPSHOT - all-in - All In Backend - - 17 - - - - org.imgscalr - imgscalr-lib - 4.2 - - - com.drewnoakes - metadata-extractor - 2.18.0 - - - org.springframework.boot - spring-boot-starter-web - - - org.springframework.boot - spring-boot-starter-test - test - - - org.springframework.boot - spring-boot-devtools - true - - - org.springframework.boot - spring-boot-starter-data-jpa - - - com.mysql - mysql-connector-j - runtime - - - org.springdoc - springdoc-openapi-starter-webmvc-ui - 2.6.0 - - - org.apache.commons - commons-math3 - 3.6.1 - - - org.jsoup - jsoup - 1.17.2 - - - com.github.javafaker - javafaker - 1.0.2 - - - com.google.firebase - firebase-admin - 9.4.1 - - - org.springframework.boot - spring-boot-starter-security - - - - - + xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> + 4.0.0 + org.springframework.boot - spring-boot-maven-plugin - - - com.diffplug.spotless - spotless-maven-plugin - 2.43.0 - - - - - - - - - - - - - - - - - org.jacoco - jacoco-maven-plugin - 0.8.12 - - - prepare-agent - - prepare-agent - - - - report - - report - - test - - - - + spring-boot-starter-parent + 3.1.5 + + + com.appdev + all-in + 0.0.1-SNAPSHOT + all-in + All In Backend + + 17 + + + + org.imgscalr + imgscalr-lib + 4.2 + + + com.drewnoakes + metadata-extractor + 2.18.0 + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-data-jpa + + + com.mysql + mysql-connector-j + runtime + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + + + org.apache.commons + commons-math3 + 3.6.1 + + + org.jsoup + jsoup + 1.17.2 + + + com.github.javafaker + javafaker + 1.0.2 + + + com.google.firebase + firebase-admin + 9.4.1 + + + org.springframework.boot + spring-boot-starter-security + + + com.twelvemonkeys.imageio + imageio-core + 3.9.4 + + + com.twelvemonkeys.imageio + imageio-webp + 3.9.4 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + com.diffplug.spotless + spotless-maven-plugin + 2.43.0 + + + + + + + + + + + + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + + report + + test + + + + - + diff --git a/src/main/java/com/appdev/allin/scrapers/PlayerScraper.java b/src/main/java/com/appdev/allin/scrapers/PlayerScraper.java index 8c60e71..435cde4 100644 --- a/src/main/java/com/appdev/allin/scrapers/PlayerScraper.java +++ b/src/main/java/com/appdev/allin/scrapers/PlayerScraper.java @@ -14,6 +14,7 @@ import com.appdev.allin.player.Player; import com.appdev.allin.player.PlayerService; import com.appdev.allin.player.Position; +import com.appdev.allin.utils.ImageProcessor; public class PlayerScraper { private static final Logger logger = LoggerFactory.getLogger(PlayerScraper.class); @@ -62,6 +63,15 @@ public void populate() throws IOException { Element imageElement = playerElement.selectFirst("div.sidearm-roster-player-image img"); String imageUrl = imageElement != null ? "https://cornellbigred.com" + imageElement.attr("data-src") : ""; + + String bucketUrl = ""; + if (!imageUrl.isEmpty()) { + String b64Image = "data:image/webp;base64," + ImageProcessor.urlToBase64(imageUrl); + if (b64Image != null && !b64Image.isEmpty()) { + bucketUrl = ImageProcessor.uploadImage(b64Image, 250, 250); + System.out.println("Image URL: " + bucketUrl); + } + } if (number == null || firstName.isEmpty() || lastName.isEmpty()) { logger.warn("Bad data for player {} {}", firstName, lastName); @@ -75,7 +85,7 @@ public void populate() throws IOException { } Player player = new Player(firstName, lastName, positions, number, height, weight, hometown, highSchool, - imageUrl); + bucketUrl); if (playerService.getPlayerByNumber(player.getNumber()) == null) { playerService.savePlayer(player); diff --git a/src/main/java/com/appdev/allin/utils/ImageProcessor.java b/src/main/java/com/appdev/allin/utils/ImageProcessor.java index 870b5be..d8edcff 100644 --- a/src/main/java/com/appdev/allin/utils/ImageProcessor.java +++ b/src/main/java/com/appdev/allin/utils/ImageProcessor.java @@ -1,226 +1,206 @@ package com.appdev.allin.utils; - -import com.drew.imaging.ImageMetadataReader; -import com.drew.metadata.Metadata; -import com.drew.metadata.exif.ExifIFD0Directory; - -import java.awt.image.BufferedImage; import java.awt.Graphics2D; import java.awt.RenderingHints; +import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; import java.io.IOException; - -import javax.imageio.ImageIO; - +import java.io.InputStream; +import java.net.URL; import java.util.Base64; import java.util.HashMap; import java.util.Map; +import javax.imageio.ImageIO; + +import org.imgscalr.Scalr; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.*; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.RestClientException; import org.springframework.web.client.RestTemplate; + +import com.drew.imaging.ImageMetadataReader; +import com.drew.metadata.Metadata; +import com.drew.metadata.exif.ExifIFD0Directory; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; - -import org.imgscalr.Scalr; - public class ImageProcessor { - @Value("${all-in.image-upload-url}") private static String imageUploadUrl; - - @Value("${all-in.image-upload-bucket}") - private static String imageUploadBucket; - - /** - * Crops the given image to the specified width and height. - * If the specified dimensions are larger than the original image, - * the method ensures that the cropped area does not exceed the image bounds. - * - * @param originalImage The original BufferedImage to be cropped. - * @param targetWidth The desired width of the cropped image. - * @param targetHeight The desired height of the cropped image. - * @return A new BufferedImage representing the cropped portion of the original - * image. - */ + @Value("${allin.bucket}") + private String bucket; public static BufferedImage cropImage(BufferedImage originalImage, int targetWidth, int targetHeight) { int originalWidth = originalImage.getWidth(); int originalHeight = originalImage.getHeight(); - - int x = (originalWidth - targetWidth) / 2; - int y = (originalHeight - targetHeight) / 2; - - x = Math.max(0, x); - y = Math.max(0, y); - + int x = Math.max(0, (originalWidth - targetWidth) / 2); + int y = Math.max(0, (originalHeight - targetHeight) / 2); targetWidth = Math.min(targetWidth, originalWidth - x); targetHeight = Math.min(targetHeight, originalHeight - y); - return originalImage.getSubimage(x, y, targetWidth, targetHeight); } - - /** - * Corrects the orientation of an image based on its EXIF metadata. - * For images with metadata specifying an orientation, the image is rotated - * to match the correct orientation. If no metadata is available or if an error - * occurs, the original image is returned. - * - * @param image The BufferedImage to correct. - * @param file The image file used to retrieve EXIF metadata. - * @return A new BufferedImage with corrected orientation, or the original image - * if no correction is necessary. - * @throws IOException If an error occurs while reading the file. - */ public static BufferedImage correctOrientation(BufferedImage image, File file) throws IOException { try { Metadata metadata = ImageMetadataReader.readMetadata(file); - if (metadata == null) { - System.err.println("No metadata found for orientation correction."); - return image; - } + if (metadata == null) return image; ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { int orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION); - switch (orientation) { - case 6: - return rotateImage(image, 90); - case 3: - return rotateImage(image, 180); - case 8: - return rotateImage(image, 270); - default: - return image; - } + return switch (orientation) { + case 6 -> rotateImage(image, 90); + case 3 -> rotateImage(image, 180); + case 8 -> rotateImage(image, 270); + default -> image; + }; } } catch (Exception e) { System.err.println("Could not determine image orientation: " + e.getMessage()); } return image; } - - /** - * Rotates image by [angle] degrees. - * - * @param image The original BufferedImage to rotate. - * @param angle The angle in degrees by which to rotate the image. Positive - * values rotate the image clockwise, and negative values rotate it - * counterclockwise. - * @return A new BufferedImage representing the rotated image. - */ public static BufferedImage rotateImage(BufferedImage image, double angle) { - // Calculate the new image dimensions after rotation double radians = Math.toRadians(angle); - int newWidth = (int) Math.abs(image.getWidth() * Math.cos(radians)) - + (int) Math.abs(image.getHeight() * Math.sin(radians)); - int newHeight = (int) Math.abs(image.getWidth() * Math.sin(radians)) - + (int) Math.abs(image.getHeight() * Math.cos(radians)); - - // Create a new image with the calculated dimensions + int newWidth = (int) Math.abs(image.getWidth() * Math.cos(radians)) + (int) Math.abs(image.getHeight() * Math.sin(radians)); + int newHeight = (int) Math.abs(image.getWidth() * Math.sin(radians)) + (int) Math.abs(image.getHeight() * Math.cos(radians)); BufferedImage rotatedImage = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB); Graphics2D g2d = rotatedImage.createGraphics(); - - // Set the rendering hints for better quality g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY); g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); - - // Calculate the rotation point (center of the original image) int x = (newWidth - image.getWidth()) / 2; int y = (newHeight - image.getHeight()) / 2; - - // Rotate around the center of the new image g2d.rotate(radians, newWidth / 2.0, newHeight / 2.0); g2d.drawImage(image, x, y, null); g2d.dispose(); - - System.out.println("Image rotated by " + angle + " degrees."); return rotatedImage; } - - /** - * Scales image to target width and height. - * - * @param originalImage The original BufferedImage to be scaled. - * @param targetWidth The desired width of the scaled image. - * @param targetHeight The desired height of the scaled image. - * @return A new BufferedImage representing the scaled image. - */ public static BufferedImage scaleImage(BufferedImage originalImage, int targetWidth, int targetHeight) { return Scalr.resize(originalImage, Scalr.Method.ULTRA_QUALITY, targetWidth, targetHeight); } - - /** - * Converts image from base64 string to BufferedImage. - * - * @param base64String The original base64 string to be converted. - * @return A BufferedImage representing the original image. - */ public static BufferedImage convertBase64ToImage(String base64String) throws IOException { - // Remove the data:image/...;base64, prefix if it exists String base64Image = base64String.split(",")[base64String.split(",").length - 1]; - byte[] imageBytes = Base64.getDecoder().decode(base64Image); try (ByteArrayInputStream bis = new ByteArrayInputStream(imageBytes)) { return ImageIO.read(bis); } } - /** - * Scales and uploads an image in base64 form to digital ocean. - * - * @param encodedImage The unmodified base64 image. - * @param targetWidth The desired width of the scaled image. - * @param targetHeight The desired height of the scaled image. - * @return A url representing the uploaded image. - */ + public static String urlToBase64(String imageUrl) throws IOException { + if (imageUrl == null || imageUrl.trim().isEmpty()) { + throw new IllegalArgumentException("Provided imageUrl is null or empty"); + } + + URL url = new URL(imageUrl); + + try (InputStream is = url.openStream(); + ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + + byte[] buffer = new byte[8192]; + int bytesRead; + + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + + byte[] imageBytes = baos.toByteArray(); + + if (imageBytes.length == 0) { + throw new IOException("No data read from the provided image URL: " + imageUrl); + } + + return Base64.getEncoder().encodeToString(imageBytes); + } + } + public static String uploadImage(String encodedImage, int targetWidth, int targetHeight) { try { - // Assume base64 starts with data URI prefix — extract format String[] parts = encodedImage.split(","); String format = "png"; - if (parts[0].contains("jpeg")) - format = "jpg"; - - // Decode base64 and convert to scaled image + if (parts[0].contains("jpeg")) format = "jpg"; + else if (parts[0].contains("webp")) format = "webp"; BufferedImage image = convertBase64ToImage(encodedImage); image = scaleImage(image, targetWidth, targetHeight); - - // Re-encode using original format + if ("webp".equals(format)) { + System.out.println("Converting WebP to PNG"); + format = "png"; + } ByteArrayOutputStream os = new ByteArrayOutputStream(); ImageIO.write(image, format, os); byte[] imageBytes = os.toByteArray(); - - // Add proper prefix - String mimeType = format.equals("jpg") ? "image/jpeg" : "image/png"; + String mimeType = switch (format) { + case "jpg" -> "image/jpeg"; + case "png" -> "image/png"; + default -> "image/png"; + }; String dataUri = "data:" + mimeType + ";base64," + Base64.getEncoder().encodeToString(imageBytes); - - // Build payload Map payload = new HashMap<>(); - payload.put("bucket", imageUploadBucket); + payload.put("bucket", "all-in"); payload.put("image", dataUri); - HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); HttpEntity> request = new HttpEntity<>(payload, headers); - RestTemplate restTemplate = new RestTemplate(); - ResponseEntity response = restTemplate.postForEntity( - imageUploadUrl, - request, - String.class); - + ResponseEntity response = restTemplate.postForEntity("https://upload.cornellappdev.com/upload/", request, String.class); if (response.getStatusCode().is2xxSuccessful()) { JsonNode json = new ObjectMapper().readTree(response.getBody()); return json.has("data") ? json.get("data").asText() : null; } else { throw new RuntimeException("Upload failed: " + response.getStatusCode()); } - } catch (IOException e) { throw new RuntimeException("Failed to upload image: " + e.getMessage(), e); } } - + public static void main(String[] args) { + try { + File file = new File("src/main/resources/static/images/players/resize.webp"); + byte[] fileBytes = new byte[(int) file.length()]; + try (FileInputStream fis = new FileInputStream(file)) { + fis.read(fileBytes); + } catch (FileNotFoundException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + String base64Image = Base64.getEncoder().encodeToString(fileBytes); + // // You can replace these with actual strings instead of env vars for testing + // String uploadUrl = System.getenv("DIGITAL_OCEAN_URL"); + // String bucket = System.getenv("BUCKET_NAME"); + // if (uploadUrl == null || bucket == null) { + // System.err.println(":x: DIGITAL_OCEAN_URL or BUCKET_NAME is not set."); + // return; + // } + String uploadedUrl = uploadImage(base64Image, 256, 256); + System.out.println(":white_check_mark: Uploaded image URL: " + uploadedUrl); + } catch (HttpServerErrorException e) { + System.err.println(":x: 500 Internal Server Error"); + System.err.println("Status Code: " + e.getStatusCode()); + System.err.println("Response Body:"); + System.err.println(e.getResponseBodyAsString()); + System.err.println("Headers:"); + System.err.println(e.getResponseHeaders()); + throw e; + } catch (HttpClientErrorException e) { + System.err.println(":x: 4xx Client Error"); + System.err.println("Status Code: " + e.getStatusCode()); + System.err.println("Response Body:"); + System.err.println(e.getResponseBodyAsString()); + System.err.println("Headers:"); + System.err.println(e.getResponseHeaders()); + throw e; + } catch (RestClientException e) { + System.err.println(":x: General RestClientException:"); + e.printStackTrace(); + throw e; + } + } } \ No newline at end of file