Skip to content

Commit

Permalink
Thumbnail (#76)
Browse files Browse the repository at this point in the history
* Add thumbnail extractors

* Add thumbnail to File Entry

* Add choosing icon or thumbnail in html
  • Loading branch information
yuliiabuchko authored May 15, 2020
1 parent a4ee123 commit 37b01f3
Show file tree
Hide file tree
Showing 10 changed files with 219 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package pl.edu.uj.ii.ksi.mordor.controllers

import java.io.ByteArrayInputStream
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import org.apache.commons.io.FileUtils
Expand All @@ -17,6 +18,7 @@ import org.springframework.web.util.UriUtils
import pl.edu.uj.ii.ksi.mordor.exceptions.BadRequestException
import pl.edu.uj.ii.ksi.mordor.exceptions.NotFoundException
import pl.edu.uj.ii.ksi.mordor.persistence.entities.Permission
import pl.edu.uj.ii.ksi.mordor.persistence.repositories.FileEntryRepository
import pl.edu.uj.ii.ksi.mordor.services.IconNameProvider
import pl.edu.uj.ii.ksi.mordor.services.repository.RepositoryDirectory
import pl.edu.uj.ii.ksi.mordor.services.repository.RepositoryEntity
Expand All @@ -27,6 +29,7 @@ import pl.edu.uj.ii.ksi.mordor.services.repository.RepositoryService
class FilesystemController(
private val repoService: RepositoryService,
private val iconNameProvider: IconNameProvider,
private val entryRepository: FileEntryRepository,
@Value("\${mordor.preview.max_text_bytes:1048576}") private val maxTextBytes: Int,
@Value("\${mordor.preview.max_image_bytes:10485760}") private val maxImageBytes: Int,
@Value("\${mordor.list_hidden_files:false}") private val listHiddenFiles: Boolean
Expand All @@ -35,7 +38,8 @@ class FilesystemController(
val path: String,
val name: String,
val iconName: String,
val relativePath: String
val relativePath: String,
val thumbnailPath: String?
)

private data class RelativeDir(
Expand Down Expand Up @@ -130,9 +134,11 @@ class FilesystemController(
val sortedChildren = entity.getChildren(canListHidden)
.sortedWith(compareBy({ it !is RepositoryDirectory }, { it.name }))
.map { entry ->
FileEntry(entry.relativePath +
if (entry is RepositoryDirectory) "/" else "",
entry.name, iconNameProvider.getIconName(entry), entity.relativePath + entry.relativePath)
FileEntry(entry.relativePath + if (entry is RepositoryDirectory) "/" else "",
entry.name,
iconNameProvider.getIconName(entry),
entity.relativePath + entry.relativePath,
if (entry is RepositoryFile && entry.thumbnail != null) entry.thumbnail else null)
}

return ModelAndView("tree", mapOf(
Expand All @@ -149,6 +155,21 @@ class FilesystemController(
return ModelAndView(RedirectView(urlEncodePath("/download/${entity.relativePath}")))
}

@Secured(Permission.READ_STR)
@GetMapping("/thumbnail/**")
fun fileThumbnail(request: HttpServletRequest, response: HttpServletResponse) {
val path = request.servletPath.removePrefix("/thumbnail")
val thumbnail = entryRepository.findById(path).get().metadata?.thumbnail?.thumbnail
response.contentType = "image/png"
thumbnail?.size?.toLong()?.let { response.setContentLengthLong(it) }

val stream = ByteArrayInputStream(thumbnail)
stream.use {
IOUtils.copy(stream, response.outputStream)
}
response.flushBuffer()
}

@Secured(Permission.READ_STR)
@GetMapping("/download/**")
fun download(request: HttpServletRequest, response: HttpServletResponse) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,10 @@ class ReviewViewsFactory(
.sortedBy { it.name }
.map { entry ->
FilesystemController.FileEntry(entry.relativePath,
entry.name, iconNameProvider.getIconName(entry), entity.relativePath + entry.relativePath)
entry.name,
iconNameProvider.getIconName(entry),
entity.relativePath + entry.relativePath,
"/thumbnail" + entry.relativePath)
}
return ModelAndView("review/session_review", mapOf(
"sessionId" to sessionId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,17 +8,20 @@ import org.springframework.transaction.annotation.Transactional
import pl.edu.uj.ii.ksi.mordor.persistence.entities.FileContent
import pl.edu.uj.ii.ksi.mordor.persistence.entities.FileEntry
import pl.edu.uj.ii.ksi.mordor.persistence.entities.FileMetadata
import pl.edu.uj.ii.ksi.mordor.persistence.entities.FileThumbnail
import pl.edu.uj.ii.ksi.mordor.persistence.repositories.FileMetadataRepository
import pl.edu.uj.ii.ksi.mordor.services.hash.FileHashProvider
import pl.edu.uj.ii.ksi.mordor.services.text.extractor.FileTextExtractor
import pl.edu.uj.ii.ksi.mordor.services.thumbnail.ThumbnailExtractor

@Service
class FileEntryCreator(
private val metadataExtractor: MetadataExtractor,
private val entityManager: EntityManager,
@Qualifier("autoDetectTextExtractor") private val fileTextExtractor: FileTextExtractor,
private val hashProvider: FileHashProvider,
private val metadataRepository: FileMetadataRepository
private val metadataRepository: FileMetadataRepository,
@Qualifier("thumbnailAutoExtractor") private val thumbnailExtractor: ThumbnailExtractor
) {

companion object {
Expand All @@ -35,21 +38,26 @@ class FileEntryCreator(
}

private fun createNewMetadata(file: File): FileEntry? {
val metadata: FileMetadata? = metadataExtractor.extract(file)
val metadata: FileMetadata = metadataExtractor.extract(file) ?: return null
val contentText: String? = fileTextExtractor.extract(file, contentMaxLength)

if (metadata == null) return null

return saveMetadata(metadata, contentText, file)
val thumbnail: ByteArray? = thumbnailExtractor.extract(file)
return saveMetadata(metadata, contentText, thumbnail, file)
}

private fun saveMetadata(metadata: FileMetadata, contentText: String?, file: File): FileEntry? {
private fun saveMetadata(
metadata: FileMetadata,
contentText: String?,
thumbnail: ByteArray?,
file: File
): FileEntry? {
entityManager.persist(metadata)
val content = FileContent(id = metadata.id, text = contentText, file = metadata)
val fileThumbnail = FileThumbnail(id = metadata.id, thumbnail = thumbnail, file = metadata)
metadata.crawledContent = content
metadata.thumbnail = fileThumbnail
val result = addEntryToExistingMetadata(metadata, file)
entityManager.persist(content)
// entityManager.persist(thumbnail)
entityManager.persist(fileThumbnail)
return result
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,10 @@ class RepositoryService(
val entry: Optional<FileEntry> = entryRepository.findById(file.path)
return if (entry.isPresent) {
val metadata = entry.get().metadata!!
// TODO: add thumbnail
val thumbnail = if (metadata.thumbnail?.thumbnail == null) null else "/thumbnail" + entry.get().path

RepositoryFile(file.name, rootPath.relativize(fullPath).toString(), file,
metadata.title, metadata.author, metadata.description, metadata.mimeType, null)
metadata.title, metadata.author, metadata.description, metadata.mimeType, thumbnail)
} else {
RepositoryFile(file.name, rootPath.relativize(fullPath).toString(), file,
null, null, null, null, null)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package pl.edu.uj.ii.ksi.mordor.services.thumbnail

import java.awt.AlphaComposite
import java.awt.Color
import java.awt.Graphics2D
import java.awt.Image
import java.awt.Toolkit
import java.awt.image.BufferedImage
import java.awt.image.FilteredImageSource
import java.awt.image.ImageFilter
import java.awt.image.RGBImageFilter
import java.io.ByteArrayOutputStream
import java.io.File
import javax.imageio.ImageIO
import org.apache.tika.Tika
import org.springframework.stereotype.Service

@Service
class ImageThumbnailExtractor(private val tika: Tika) : ThumbnailExtractor() {
override fun extract(file: File): ByteArray? {
return extract(ImageIO.read(file))
}

override fun canParse(file: File): Boolean {
return tika.detect(file).startsWith("image")
}

fun extract(image: BufferedImage): ByteArray? {
val thumbnail = getTransparentScaledImage(image, width, height)
val bos = ByteArrayOutputStream()
ImageIO.write(thumbnail, "png", bos)
return bos.toByteArray()
}

private fun getTransparentScaledImage(image: BufferedImage, finalWidth: Int, finalHeight: Int): BufferedImage {
val scaledWidth = computeScaledWidth(image, finalWidth, finalHeight)
val scaledHeight = computeScaledHeight(image, finalWidth, finalHeight)

val scaledImg = BufferedImage(finalWidth, finalHeight, BufferedImage.TYPE_INT_RGB)
val transparentImg = BufferedImage(finalWidth, finalHeight, BufferedImage.TYPE_INT_ARGB)

initGraphics2D(scaledImg, finalWidth, finalHeight)
.drawImage(image.getScaledInstance(scaledWidth, scaledHeight, Image.SCALE_SMOOTH),
0, 0, null)

initGraphics2D(transparentImg, finalWidth, finalHeight)
.drawImage(makeColorTransparent(scaledImg, Color(0, 0, 0, 0)),
(finalWidth - scaledWidth) / 2, (finalHeight - scaledHeight) / 2,
finalWidth, finalHeight, Color(0, 0, 0, 0), null)

return transparentImg
}

private fun computeScaledWidth(image: BufferedImage, finalWidth: Int, finalHeight: Int): Int {
if (image.width < image.height) {
return (image.width * finalHeight / image.height)
}
return finalWidth
}

private fun computeScaledHeight(image: BufferedImage, finalWidth: Int, finalHeight: Int): Int {
if (image.width > image.height) {
return (image.height * finalWidth / image.width)
}
return finalHeight
}

private fun initGraphics2D(image: BufferedImage, finalWidth: Int, finalHeight: Int): Graphics2D {
val graphics2D = image.createGraphics()
graphics2D.composite = AlphaComposite.SrcOver
graphics2D.color = Color(0, 0, 0, 0)
graphics2D.fillRect(0, 0, finalWidth, finalHeight)
return graphics2D
}

private fun makeColorTransparent(image: BufferedImage, color: Color): Image {
val markerRGB = color.rgb or -0x1000000

val filter: ImageFilter = object : RGBImageFilter() {
override fun filterRGB(x: Int, y: Int, rgb: Int): Int {
return if (rgb or -0x1000000 == markerRGB) {
transparent and rgb
} else {
rgb
}
}
}
return Toolkit.getDefaultToolkit().createImage(FilteredImageSource(image.source, filter))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package pl.edu.uj.ii.ksi.mordor.services.thumbnail

import java.io.File
import org.apache.pdfbox.pdmodel.PDDocument
import org.apache.pdfbox.rendering.PDFRenderer
import org.apache.tika.Tika
import org.springframework.stereotype.Service

@Service
class PDFThumbnailExtractor(private val tika: Tika) : ThumbnailExtractor() {
override fun extract(file: File): ByteArray? {
val doc = PDDocument.load(file)
doc.use {
return ImageThumbnailExtractor(tika).extract(PDFRenderer(doc).renderImage(0))
}
}

override fun canParse(file: File): Boolean {
return tika.detect(file) == "application/pdf"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package pl.edu.uj.ii.ksi.mordor.services.thumbnail

import java.io.File
import org.apache.tika.Tika
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import pl.edu.uj.ii.ksi.mordor.services.repository.RepositoryService

@Service
class ThumbnailAutoExtractor(tika: Tika) : ThumbnailExtractor() {

private val extractors = listOf(ImageThumbnailExtractor(tika), PDFThumbnailExtractor(tika))

companion object {
private val logger = LoggerFactory.getLogger(RepositoryService::class.java)
}

override fun extract(file: File): ByteArray? {
for (extractor in extractors) {
if (extractor.canParse(file)) {
logger.info("Extracting thumbnail for " + file.absolutePath + " using " + extractor.javaClass.name)
return extractor.extract(file)
}
}
logger.warn("No thumbnail extractor for file " + file.absolutePath)
return null
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package pl.edu.uj.ii.ksi.mordor.services.thumbnail

import java.io.File

abstract class ThumbnailExtractor {

val width: Int
get() = 200

val height: Int
get() = 200

val transparent: Int
get() = 0x00FFFFFF

open fun extract(file: File): ByteArray? {
return null
}

open fun canParse(file: File): Boolean {
return true
}
}
5 changes: 5 additions & 0 deletions src/main/resources/static/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -48,3 +48,8 @@ code.hljs {
outline: none;
overflow: hidden;
}

.thumbnail {
width: 30px;
height: 30px;
}
6 changes: 4 additions & 2 deletions src/main/resources/templates/tree.html
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@

<ul th:unless="${children.empty}" class="list-group list-group-flush">
<th:block th:each="entry, iter : ${children}">
<li class="list-group-item">
<i th:attr="class='mx-2 fas fa-lg ' + ${entry.iconName}"></i>
<li class="list-group-item align-items-center">
<i th:if="${entry.thumbnailPath == null}" th:attr="class='mx-2 fas fa-lg ' + ${entry.iconName}"></i>
<img th:if="${entry.thumbnailPath != null}" th:attr="class='thumbnail'" th:src="${entry.thumbnailPath}"/>

<a th:attr="href='/file/'+${entry.path}" th:text="${entry.name}"></a>

<a class="btn btn-danger float-right"
Expand Down

0 comments on commit 37b01f3

Please sign in to comment.