From 11f77cf995be5357f023dc67d85970fd55343fae Mon Sep 17 00:00:00 2001 From: Tim Holloway Date: Tue, 28 Dec 2021 21:12:23 -0500 Subject: [PATCH] Thumbnails! --- pom.xml | 12 +- .../persistence/dao/RecipeRepository.java | 2 + .../persistence/service/RecipeService.java | 9 +- .../gourmetj/springweb/PictureController.java | 281 ++++++++++++++++++ .../gourmetj/springweb/package-info.java | 11 + .../resources/WEB-INF/layout/layout.xhtml | 6 +- .../META-INF/resources/css/style.css | 4 + .../resources/META-INF/resources/main.xhtml | 17 +- 8 files changed, 331 insertions(+), 11 deletions(-) create mode 100644 src/main/java/com/mousetech/gourmetj/springweb/PictureController.java create mode 100644 src/main/java/com/mousetech/gourmetj/springweb/package-info.java create mode 100644 src/main/resources/META-INF/resources/css/style.css diff --git a/pom.xml b/pom.xml index a91b077..11b4222 100644 --- a/pom.xml +++ b/pom.xml @@ -56,7 +56,17 @@ javax.validation validation-api - 2.0.1.Final + + + + org.hibernate.validator + hibernate-validator + + + + + org.springframework.boot + spring-boot-starter-web diff --git a/src/main/java/com/mousetech/gourmetj/persistence/dao/RecipeRepository.java b/src/main/java/com/mousetech/gourmetj/persistence/dao/RecipeRepository.java index 58ad1d9..3c2aefa 100644 --- a/src/main/java/com/mousetech/gourmetj/persistence/dao/RecipeRepository.java +++ b/src/main/java/com/mousetech/gourmetj/persistence/dao/RecipeRepository.java @@ -23,6 +23,8 @@ import com.mousetech.gourmetj.persistence.model.Recipe; public interface RecipeRepository extends JpaRepository { + List findByTitleContaining(String searchText); + // final static String SQL_FIND_CATEGORIES = // "SELECT DISTINCT category from categories" // + " where category is not null and category <> ''" diff --git a/src/main/java/com/mousetech/gourmetj/persistence/service/RecipeService.java b/src/main/java/com/mousetech/gourmetj/persistence/service/RecipeService.java index 84fe860..c4d73f0 100644 --- a/src/main/java/com/mousetech/gourmetj/persistence/service/RecipeService.java +++ b/src/main/java/com/mousetech/gourmetj/persistence/service/RecipeService.java @@ -2,6 +2,8 @@ package com.mousetech.gourmetj.persistence.service; import java.io.Serializable; import java.util.List; +import java.util.Optional; + import javax.enterprise.context.ApplicationScoped; import javax.inject.Inject; import javax.inject.Named; @@ -33,8 +35,11 @@ public class RecipeService implements Serializable { } public List findByTitle(String searchText) { - // TODO Auto-generated method stub - return null; + return recipeRepository.findByTitleContaining(searchText); + } + + public Recipe findByPrimaryKey(Long recipeId) { + return recipeRepository.findById(recipeId).orElse(null); } // /** diff --git a/src/main/java/com/mousetech/gourmetj/springweb/PictureController.java b/src/main/java/com/mousetech/gourmetj/springweb/PictureController.java new file mode 100644 index 0000000..1df8748 --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/springweb/PictureController.java @@ -0,0 +1,281 @@ +/** + * Copyright (C) 2021, Tim Holloway + * + * Date written: Nov 26, 2021 + * Author: Tim Holloway + */ +package com.mousetech.gourmetj.springweb; + +import java.awt.Color; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; + +import javax.imageio.ImageIO; +import javax.imageio.stream.ImageInputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpSession; +import javax.servlet.http.Part; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestMapping; + +import com.mousetech.gourmetj.UserSession; +import com.mousetech.gourmetj.persistence.model.Recipe; +import com.mousetech.gourmetj.persistence.service.RecipeService; + +/** + * @author timh + * @since Nov 26, 2021 + */ +@Controller +@RequestMapping(path="/img") +public class PictureController { + + /* Logger */ + private static final Logger log = + LoggerFactory.getLogger(PictureController.class); + + @Autowired + private RecipeService recipeService; + + /** + * @return the recipeService + */ + public RecipeService getRecipeService() { + return recipeService; + } + + /** + * @param recipeService the recipeService to set + */ + public void setRecipeService(RecipeService recipeService) { + this.recipeService = recipeService; + } + + // *** + /** + * Default image returned if a recipe has no actual image. + * It's thumbnail-sized, so does double duty. + */ + static byte[] BLANK_IMAGE; + + public PictureController() { + init(); + } + + /** + * General setup. Constructs BLANK_IMAGE. + */ + private void init() { + BufferedImage tbi = new BufferedImage(40, 40, + BufferedImage.TYPE_INT_ARGB); + Graphics2D ig2 = tbi.createGraphics(); + ig2.setBackground(Color.WHITE); + ig2.clearRect(0, 0, 40, 40); + + try { + ByteArrayOutputStream os = + new ByteArrayOutputStream(946); + ImageIO.write(tbi, "png", os); + BLANK_IMAGE = os.toByteArray(); + } catch (IOException e) { + log.error("Unable to create BLANK_IMAGE", e); + } + } + + // ========================================== + @GetMapping(value = "/picture/{recipeId}") + @PostMapping(value = "/picture/{recipeId}") + public ResponseEntity bigPicture( + @PathVariable long recipeId) { + + Recipe recipe = recipeService.findByPrimaryKey(recipeId); + if (recipe != null) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE); + httpHeaders.set(HttpHeaders.CACHE_CONTROL, + "no-cache"); + byte[] image = recipe.getImage(); + if (image == null) { + image = BLANK_IMAGE; + } + return ResponseEntity.ok().headers(httpHeaders) + .body(image); + } + + // NotFound, need dummy picture!: + return ResponseEntity.status(404).body(new byte[0]); + } + + /** + * Dummy picture request. Made when recipeId is not yet + * assigned. + * + * @param req The HttpServletRequest from Spring. + * @return Image response. + */ + @GetMapping(value = "/picture/") + public ResponseEntity bigPicture( + HttpServletRequest req) { + + final HttpSession httpSession = req.getSession(false); + final UserSession userSession = (UserSession) httpSession + .getAttribute("userSession"); + + Recipe editRecipe = userSession.getRecipe(); + + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE); + httpHeaders.set(HttpHeaders.CACHE_CONTROL, "no-cache"); + byte[] image = editRecipe.getImage(); + if (image == null) { + image = BLANK_IMAGE; + } + return ResponseEntity.ok().headers(httpHeaders) + .body(image); + } + + @GetMapping(value = "/thumb/{recipeId}") + public ResponseEntity thumbnail( + @PathVariable long recipeId) { + + Recipe recipe = recipeService.findByPrimaryKey(recipeId); + if (recipe != null) { + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.set(HttpHeaders.CONTENT_TYPE, + MediaType.IMAGE_PNG_VALUE); + byte[] image = recipe.getThumb(); + // Empty thumbs have been seen with "H"! + if (image == null || image.length < 2) { + image = BLANK_IMAGE; + } + return ResponseEntity.ok().headers(httpHeaders) + .body(image); + } + + // NotFound, need dummy picture!: + return ResponseEntity.status(404).body(new byte[0]); + } + + /** + * Take imageFile input and store it as image for recipe. + * Generate thumnail + * + * @param recipe Recipe to store into. + * @param imageFile Info about uploaded data. + * + * CalledFrom @see + * RecipeDetailBean#ajaxUploadImage(AjaxBehaviorEvent + * event) + */ + public static void importImage(Recipe recipe, + Part imageFile) { + // imageFile.getContentType(); // ex: image/jpeg + try { + byte[] bytes = null; + + InputStream istream = imageFile.getInputStream(); + ImageInputStream stream = + ImageIO.createImageInputStream(istream); + BufferedImage bi = ImageIO.read(stream); + + if ((bi.getWidth() > 512) || (bi.getWidth() > 512)) { + bi = scaleImageSanely(bi); + } + + ByteArrayOutputStream baos = + new ByteArrayOutputStream(); + ImageIO.write(bi, "png", baos); + bytes = baos.toByteArray(); + recipe.setImage(bytes); + + final int thumbHeight = 40; + float ratio = ((float) bi.getHeight()) / thumbHeight; + int thumbWidth = (int) (bi.getWidth() / ratio); + + BufferedImage resizedImg = + new BufferedImage(thumbWidth, thumbHeight, + BufferedImage.TRANSLUCENT); + + // Create a device-independent object to draw the + // resized image + Graphics2D g2 = resizedImg.createGraphics(); + + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + + // Finally draw the source image in the Graphics2D + // with the desired size. + g2.drawImage(bi, 0, 0, thumbWidth, thumbHeight, + null); + + // Disposes of this graphics context and releases any + // system resources that it is using + g2.dispose(); + + baos = new ByteArrayOutputStream(); + ImageIO.write(resizedImg, "png", baos); + bytes = baos.toByteArray(); + recipe.setThumb(bytes); + } catch (IOException e) { + // TODO Auto-generated catch block + e.printStackTrace(); + } + } + + /** + * Reduce image so that width or height fit. + * + * @param bi + * + * @return + */ + private static BufferedImage scaleImageSanely( + BufferedImage bi) { + int w0 = bi.getWidth(); + int h0 = bi.getHeight(); + + int w1 = w0; + int h1 = h0; + + double ratio = 1.0; + + if (h0 > 512) { + h1 = 512; + ratio = ((double) h0) / ((double) h1); + w1 = (int) (w0 / ratio); + } + if (w1 > 512) { + w0 = w1; + w1 = 512; + ratio = ((double) w0) / ((double) w1); + w1 = (int) (w0 / ratio); + } + + BufferedImage resizedImg = new BufferedImage(w1, h1, + BufferedImage.TRANSLUCENT); + + Graphics2D g2 = resizedImg.createGraphics(); + g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, + RenderingHints.VALUE_INTERPOLATION_BILINEAR); + g2.drawImage(bi, 0, 0, w1, h1, null); + g2.dispose(); + + return resizedImg; + } +} diff --git a/src/main/java/com/mousetech/gourmetj/springweb/package-info.java b/src/main/java/com/mousetech/gourmetj/springweb/package-info.java new file mode 100644 index 0000000..61dc70b --- /dev/null +++ b/src/main/java/com/mousetech/gourmetj/springweb/package-info.java @@ -0,0 +1,11 @@ +/** + * Copyright (C) 2021, Tim Holloway + * + * Date written: Dec 28, 2021 + * Author: Tim Holloway + */ +/** + * @author timh + * @since Dec 28, 2021 + */ +package com.mousetech.gourmetj.springweb; diff --git a/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml b/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml index 21a2247..038b97b 100644 --- a/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml +++ b/src/main/resources/META-INF/resources/WEB-INF/layout/layout.xhtml @@ -5,10 +5,14 @@ xmlns:tc="http://myfaces.apache.org/tobago/component" > + <ui:insert name="title">Gourmet Recipe Manager (web version)</ui:insert> + + +

Gourmet Recipe Manager (web version)

@@ -20,6 +24,6 @@ href="http://www.apache.org/licenses/LICENSE-2.0" >Apache License, Version 2.0.

Based on Gourmet Recipe Manager by T. Hinkle

- +
\ No newline at end of file diff --git a/src/main/resources/META-INF/resources/css/style.css b/src/main/resources/META-INF/resources/css/style.css new file mode 100644 index 0000000..135bc8c --- /dev/null +++ b/src/main/resources/META-INF/resources/css/style.css @@ -0,0 +1,4 @@ +.redBox { + color: red; + background-color: blue; +} diff --git a/src/main/resources/META-INF/resources/main.xhtml b/src/main/resources/META-INF/resources/main.xhtml index 948567d..bc8c3a2 100644 --- a/src/main/resources/META-INF/resources/main.xhtml +++ b/src/main/resources/META-INF/resources/main.xhtml @@ -13,16 +13,19 @@ - + - - @@ -33,13 +36,13 @@ value="#{adminMainBean.searchResults}" var="row" > - - +