Skip to content

Commit f0338a0

Browse files
authored
Merge pull request #802 from NDLANO/chore/raw-controller-scrimage
2 parents ba6c942 + 8311d64 commit f0338a0

File tree

15 files changed

+302
-329
lines changed

15 files changed

+302
-329
lines changed

image-api/package.mill

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,6 @@ object `package` extends BaseModule, DockerComponent {
1616
SharedDependencies.scalaUri,
1717
SharedDependencies.awsS3,
1818
SharedDependencies.jsoup,
19-
mvn"org.imgscalr:imgscalr-lib:4.2",
20-
// These are not strictly needed, for most cases, but offers better handling of loading images with encoding issues
21-
mvn"com.twelvemonkeys.imageio:imageio-core:3.12.0",
22-
mvn"com.twelvemonkeys.imageio:imageio-jpeg:3.12.0",
2319
mvn"com.sksamuel.scrimage:scrimage-core:4.3.5",
2420
mvn"com.sksamuel.scrimage:scrimage-webp:4.3.5",
2521
mvn"commons-io:commons-io:2.19.0",

image-api/src/main/scala/no/ndla/imageapi/controller/RawController.scala

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@ package no.ndla.imageapi.controller
1010

1111
import cats.implicits.catsSyntaxEitherId
1212
import no.ndla.common.errors.{ValidationException, ValidationMessage}
13-
import no.ndla.imageapi.model.domain.ImageStream
14-
import no.ndla.imageapi.service.{ImageConverter, ImageStorageService, PercentPoint, PixelPoint, ReadService}
13+
import no.ndla.imageapi.model.domain.{ImageStream, ProcessableImageStream, UnprocessableImageStream}
14+
import no.ndla.imageapi.service.*
1515
import no.ndla.network.clients.MyNDLAApiClient
16-
import no.ndla.network.tapir.{AllErrors, ErrorHandling, DynamicHeaders, ErrorHelpers, TapirController}
1716
import no.ndla.network.tapir.TapirUtil.errorOutputsFor
17+
import no.ndla.network.tapir.*
1818
import sttp.tapir.*
1919
import sttp.tapir.server.ServerEndpoint
2020

@@ -29,8 +29,8 @@ class RawController(using
2929
readService: ReadService,
3030
myNDLAApiClient: MyNDLAApiClient,
3131
) extends TapirController {
32-
import errorHelpers.*
3332
import errorHandling.*
33+
import errorHelpers.*
3434
override val serviceName: String = "raw"
3535
override val prefix: EndpointInput[Unit] = "image-api" / serviceName
3636
override val enableSwagger: Boolean = true
@@ -39,7 +39,7 @@ class RawController(using
3939

4040
private def toImageResponse(image: ImageStream): Either[AllErrors, (DynamicHeaders, InputStream)] = {
4141
val headers = DynamicHeaders.fromValue("Content-Type", image.contentType)
42-
Right(headers -> image.stream)
42+
Right(headers -> image.toStream)
4343
}
4444

4545
def getImageFile: ServerEndpoint[Any, Eff] = endpoint
@@ -69,9 +69,7 @@ class RawController(using
6969
.out(inputStreamBody)
7070
.serverLogicPure { case (imageId, imageParams) =>
7171
readService.getImageFileName(imageId, imageParams.language) match {
72-
case Success(Some(fileName)) =>
73-
val x = getRawImage(fileName, imageParams)
74-
x match {
72+
case Success(Some(fileName)) => getRawImage(fileName, imageParams) match {
7573
case Failure(ex) => returnLeftError(ex)
7674
case Success(img) => toImageResponse(img)
7775
}
@@ -88,18 +86,19 @@ class RawController(using
8886
resize
8987
}
9088
}
91-
val nonResizableMimeTypes = List("image/gif", "image/svg", "image/svg+xml")
9289
imageStorage.get(imageName) match {
93-
case Success(img) if nonResizableMimeTypes.contains(img.contentType.toLowerCase) => Success(img)
94-
case Success(img) => crop(img, imageParams)
90+
case Success(img: UnprocessableImageStream) => Success(img)
91+
case Success(img: ProcessableImageStream) => crop(img, imageParams)
9592
.flatMap(stream => dynamicCropOrResize(stream, imageParams))
9693
.recoverWith {
9794
case ex: ValidationException => Failure(ex)
9895
case ex =>
9996
logger.error(s"Could not crop or resize image '$imageName', got exception: '${ex.getMessage}'", ex)
10097
Success(img)
10198
}
102-
case Failure(e) => Failure(e)
99+
case Failure(ex) =>
100+
logger.error(s"Failed to get image '$imageName' from S3", ex)
101+
Failure(ex)
103102
}
104103
}
105104

@@ -113,7 +112,7 @@ class RawController(using
113112
}
114113
}
115114

116-
private def crop(image: ImageStream, imageParams: ImageParams): Try[ImageStream] = {
115+
private def crop(image: ProcessableImageStream, imageParams: ImageParams): Try[ProcessableImageStream] = {
117116
val unit = imageParams.cropUnit.getOrElse("percent")
118117
unit match {
119118
case "percent" =>
@@ -145,7 +144,7 @@ class RawController(using
145144
.isDefined || imageParams.ratio.isDefined)
146145
}
147146

148-
private def dynamicCrop(image: ImageStream, imageParams: ImageParams): Try[ImageStream] = {
147+
private def dynamicCrop(image: ProcessableImageStream, imageParams: ImageParams): Try[ProcessableImageStream] = {
149148

150149
(imageParams.focalX, imageParams.focalY, imageParams.width, imageParams.height) match {
151150
case (Some(fx), Some(fy), w, h) =>
@@ -154,7 +153,7 @@ class RawController(using
154153
}
155154
}
156155

157-
private def resize(image: ImageStream, imageParams: ImageParams): Try[ImageStream] = {
156+
private def resize(image: ProcessableImageStream, imageParams: ImageParams): Try[ProcessableImageStream] = {
158157
(imageParams.width, imageParams.height) match {
159158
case (Some(width), Some(height)) => imageConverter.resize(image, width.toInt, height.toInt)
160159
case (Some(width), _) => imageConverter.resizeWidth(image, width.toInt)

image-api/src/main/scala/no/ndla/imageapi/model/NDLAErrors.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ case class ImageDeleteException(message: String, exs: Seq[Throwable]) ex
2323
case class ImageVariantsUploadException(message: String, exs: Seq[Throwable]) extends MultipleExceptions(message, exs)
2424
case class ImageConversionException(message: String) extends RuntimeException(message)
2525
case class ImageCopyException(message: String) extends RuntimeException(message)
26-
case class ImageInvalidFormat(message: String) extends RuntimeException(message)
26+
case class ImageUnprocessableFormatException(contentType: String)
27+
extends RuntimeException(s"Image of '$contentType' Content-Type did not have a processable binary format")
2728

2829
object ImageErrorHelpers {
2930
def fileTooBigError(using props: Props): String =

image-api/src/main/scala/no/ndla/imageapi/model/domain/ImageStream.scala

Lines changed: 38 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,44 @@
88

99
package no.ndla.imageapi.model.domain
1010

11-
import java.awt.image.BufferedImage
12-
import java.io.InputStream
11+
import com.sksamuel.scrimage.ImmutableImage
12+
import com.sksamuel.scrimage.nio.{JpegWriter, PngWriter}
13+
import com.sksamuel.scrimage.webp.WebpWriter
1314

14-
trait ImageStream {
15-
def contentType: String
16-
def stream: InputStream
15+
import java.io.{ByteArrayInputStream, InputStream}
16+
import scala.util.Try
17+
18+
sealed trait ImageStream {
19+
def toStream: InputStream
1720
def fileName: String
18-
def format: String = fileName.substring(fileName.lastIndexOf(".") + 1)
19-
lazy val sourceImage: BufferedImage
21+
def contentType: String
22+
}
23+
24+
final case class ProcessableImageStream(
25+
image: ImmutableImage,
26+
override val fileName: String,
27+
format: ProcessableImageFormat,
28+
) extends ImageStream {
29+
override def toStream: InputStream = {
30+
val writer = format match {
31+
case ProcessableImageFormat.Jpeg => JpegWriter.Default
32+
case ProcessableImageFormat.Png => PngWriter.MaxCompression
33+
case ProcessableImageFormat.Webp => WebpWriter.DEFAULT
34+
}
35+
val bytes = image.bytes(writer)
36+
new ByteArrayInputStream(bytes)
37+
}
38+
39+
override val contentType: String = format.toContentType
40+
41+
def transform(f: ImmutableImage => ImmutableImage): Try[ProcessableImageStream] =
42+
Try(f(image)).map(transformed => copy(image = transformed))
43+
}
44+
45+
final case class UnprocessableImageStream(
46+
imageBytes: Array[Byte],
47+
override val fileName: String,
48+
override val contentType: String,
49+
) extends ImageStream {
50+
override def toStream: InputStream = new ByteArrayInputStream(imageBytes)
2051
}

image-api/src/main/scala/no/ndla/imageapi/model/domain/ProcessableImageFormat.scala

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ package no.ndla.imageapi.model.domain
1111
import com.sksamuel.scrimage.format.Format
1212
import enumeratum.{Enum, EnumEntry}
1313

14-
sealed trait ProcessableImageFormat extends EnumEntry
14+
sealed trait ProcessableImageFormat extends EnumEntry {
15+
def toContentType: String = this match {
16+
case ProcessableImageFormat.Jpeg => "image/jpeg"
17+
case ProcessableImageFormat.Png => "image/png"
18+
case ProcessableImageFormat.Webp => "image/webp"
19+
}
20+
}
1521

1622
object ProcessableImageFormat extends Enum[ProcessableImageFormat] {
1723
case object Jpeg extends ProcessableImageFormat

0 commit comments

Comments
 (0)