Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 2 additions & 13 deletions image-api/src/main/scala/no/ndla/imageapi/ImageApiProperties.scala
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ package no.ndla.imageapi
import com.typesafe.scalalogging.StrictLogging
import no.ndla.common.configuration.BaseProps
import no.ndla.database.DatabaseProps
import no.ndla.imageapi.model.domain.ImageContentType
import no.ndla.network.{AuthUser, Domains}

import scala.util.Properties.*
Expand All @@ -31,19 +32,7 @@ class ImageApiProperties extends BaseProps with DatabaseProps with StrictLogging
val ImageControllerV3Path: String = s"$ImageApiBasePath/v3/images"
val RawControllerPath: String = s"$ImageApiBasePath/raw"

val ValidFileExtensions: Seq[String] = Seq(".jpg", ".png", ".jpeg", ".bmp", ".gif", ".svg", ".jfif")

val ValidMimeTypes: Seq[String] = Seq(
"image/bmp",
"image/gif",
"image/jpeg",
"image/x-citrix-jpeg",
"image/pjpeg",
"image/png",
"image/x-citrix-png",
"image/x-png",
"image/svg+xml",
)
val ValidMimeTypes: Seq[ImageContentType] = ImageContentType.values

val IsoMappingCacheAgeInMs: Int = 1000 * 60 * 60 // 1 hour caching
val LicenseMappingCacheAgeInMs: Int = 1000 * 60 * 60 // 1 hour caching
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ trait BaseImageController(using props: Props) {
val heightTo: EndpointInput.Query[Option[Int]] =
query[Option[Int]]("height-to").description("Filter images with height less than or equal to this value.")

val contentType: EndpointInput.Query[Option[String]] = query[Option[String]]("content-type").description(
"Filter images by content type (e.g., 'image/jpeg', 'image/png')."
)

val maxImageFileSizeBytes: Int = props.MaxImageFileSizeBytes

def doWithStream[T](filePart: Part[File])(f: UploadedFile => Try[T]): Try[T] = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ class ImageControllerV2(using
widthTo: Option[Int],
heightFrom: Option[Int],
heightTo: Option[Int],
contentType: Option[String],
) = {
val settings = query match {
case Some(searchString) => SearchSettings(
Expand All @@ -120,6 +121,7 @@ class ImageControllerV2(using
widthTo = widthTo,
heightFrom = heightFrom,
heightTo = heightTo,
contentType = contentType,
)
case None => SearchSettings(
query = None,
Expand All @@ -139,6 +141,7 @@ class ImageControllerV2(using
widthTo = widthTo,
heightFrom = heightFrom,
heightTo = heightTo,
contentType = contentType,
)
}

Expand Down Expand Up @@ -171,6 +174,7 @@ class ImageControllerV2(using
.in(widthTo)
.in(heightFrom)
.in(heightTo)
.in(contentType)
.errorOut(errorOutputsFor(400))
.out(jsonBody[SearchResultDTO])
.out(EndpointOutput.derived[DynamicHeaders])
Expand All @@ -194,6 +198,7 @@ class ImageControllerV2(using
widthTo,
heightFrom,
heightTo,
contentType,
) => scrollSearchOr(scrollId, language, user) {
val sort = Sort.valueOf(sortStr)
val shouldScroll = scrollId.exists(props.InitialScrollContextKeywords.contains)
Expand All @@ -217,6 +222,7 @@ class ImageControllerV2(using
widthTo,
heightFrom,
heightTo,
contentType,
)
}.handleErrorsOrOk
}
Expand Down Expand Up @@ -250,6 +256,7 @@ class ImageControllerV2(using
val widthTo = searchParams.widthTo
val heightFrom = searchParams.heightFrom
val heightTo = searchParams.heightTo
val contentType = searchParams.contentType

search(
minimumSize,
Expand All @@ -269,6 +276,7 @@ class ImageControllerV2(using
widthTo,
heightFrom,
heightTo,
contentType,
)
}.handleErrorsOrOk
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ class ImageControllerV3(using
widthTo: Option[Int],
heightFrom: Option[Int],
heightTo: Option[Int],
contentType: Option[String],
) = {
val settings = query match {
case Some(searchString) => SearchSettings(
Expand All @@ -108,6 +109,7 @@ class ImageControllerV3(using
widthTo = widthTo,
heightFrom = heightFrom,
heightTo = heightTo,
contentType = contentType,
)
case None => SearchSettings(
query = None,
Expand All @@ -127,6 +129,7 @@ class ImageControllerV3(using
widthTo = widthTo,
heightFrom = heightFrom,
heightTo = heightTo,
contentType = contentType,
)
}
for {
Expand Down Expand Up @@ -158,6 +161,7 @@ class ImageControllerV3(using
.in(widthTo)
.in(heightFrom)
.in(heightTo)
.in(contentType)
.errorOut(errorOutputsFor(400))
.out(jsonBody[SearchResultV3DTO])
.out(EndpointOutput.derived[DynamicHeaders])
Expand All @@ -183,6 +187,7 @@ class ImageControllerV3(using
widthTo,
heightFrom,
heightTo,
contentType,
) => scrollSearchOr(scrollId, language.code, user) {
val sort = Sort.valueOf(sortStr)
val shouldScroll = scrollId.exists(props.InitialScrollContextKeywords.contains)
Expand All @@ -208,6 +213,7 @@ class ImageControllerV3(using
widthTo,
heightFrom,
heightTo,
contentType,
)
}.handleErrorsOrOk
}
Expand Down Expand Up @@ -248,6 +254,7 @@ class ImageControllerV3(using
val widthTo = searchParams.widthTo
val heightFrom = searchParams.heightFrom
val heightTo = searchParams.heightTo
val contentType = searchParams.contentType

searchV3(
minimumSize,
Expand All @@ -268,6 +275,7 @@ class ImageControllerV3(using
widthTo,
heightFrom,
heightTo,
contentType,
)
}.handleErrorsOrOk
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ class RawController(using
override val endpoints: List[ServerEndpoint[Any, Eff]] = List(getImageFileById, getImageFile)

private def toImageResponse(image: ImageStream): Either[AllErrors, (DynamicHeaders, InputStream)] = {
val headers =
DynamicHeaders.fromValues("Content-Type" -> image.contentType, "Content-Length" -> image.contentLength.toString)
val headers = DynamicHeaders.fromValues(
"Content-Type" -> image.contentType.toString,
"Content-Length" -> image.contentLength.toString,
)
Right(headers -> image.stream)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ case class ImageDeleteException(message: String, exs: Seq[Throwable]) ex
case class ImageVariantsUploadException(message: String, exs: Seq[Throwable]) extends MultipleExceptions(message, exs)
case class ImageConversionException(message: String) extends RuntimeException(message)
case class ImageCopyException(message: String) extends RuntimeException(message)
case class ImageContentTypeException(message: String) extends RuntimeException(message)
case class ImageUnprocessableFormatException(contentType: String)
extends RuntimeException(s"Image of '$contentType' Content-Type did not have a processable binary format")
extends RuntimeException(s"Image of '${contentType}' Content-Type did not have a processable binary format")

object ImageErrorHelpers {
def fileTooBigError(using props: Props): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ package no.ndla.imageapi.model.api

import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import io.circe.{Decoder, Encoder}
import no.ndla.imageapi.model.domain.ImageContentType
import no.ndla.language.Language.LanguageDocString
import sttp.tapir.Schema.annotations.description

Expand All @@ -20,7 +21,7 @@ case class ImageFileDTO(
@description("The size of the image in bytes")
size: Long,
@description("The mimetype of the image")
contentType: String,
contentType: ImageContentType,
@description("The full url to where the image can be downloaded")
imageUrl: String,
@description("Dimensions of the image")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import io.circe.{Decoder, Encoder}
import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder}
import no.ndla.common.model.NDLADate
import no.ndla.common.model.api.CopyrightDTO
import no.ndla.imageapi.model.domain.ImageContentType
import sttp.tapir.Schema.annotations.description

@description("Meta information for the image")
Expand All @@ -29,7 +30,7 @@ case class ImageMetaInformationV2DTO(
@description("The size of the image in bytes")
size: Long,
@description("The mimetype of the image")
contentType: String,
contentType: ImageContentType,
@description("Describes the copyright information for the image")
copyright: CopyrightDTO,
@description("Searchable tags for the image")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ case class SearchParamsDTO(
heightFrom: Option[Int],
@description("Filter images with height less than or equal to this value.")
heightTo: Option[Int],
@description("Filter images by content type (e.g., 'image/jpeg', 'image/png').")
contentType: Option[String],
)

object SearchParamsDTO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ object ImageCaption {
case class UploadedImage(
fileName: String,
size: Long,
contentType: String,
contentType: ImageContentType,
dimensions: Option[ImageDimensions],
variants: Seq[ImageVariant],
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Part of NDLA image-api
* Copyright (C) 2026 NDLA
*
* See LICENSE
*
*/

package no.ndla.imageapi.model.domain

import enumeratum.{Enum, EnumEntry}
import no.ndla.common.CirceUtil.CirceEnumWithErrors

sealed abstract class ImageContentType(override val entryName: String, val fileEndings: List[String])
extends EnumEntry {
override def toString: String = entryName
}

object ImageContentType extends Enum[ImageContentType], CirceEnumWithErrors[ImageContentType] {
case object Bmp extends ImageContentType("image/bmp", List(".bmp"))
case object Gif extends ImageContentType("image/gif", List(".gif"))
case object Jpeg extends ImageContentType("image/jpeg", List(".jpg", ".jpeg"))
case object JpegCitrix extends ImageContentType("image/x-citrix-jpeg", List(".jpg", ".jpeg"))
case object JpegProgressive extends ImageContentType("image/pjpeg", List(".jpg", ".jpeg"))
case object Png extends ImageContentType("image/png", List(".png"))
case object PngXToken extends ImageContentType("image/x-png", List(".png"))
case object Svg extends ImageContentType("image/svg+xml", List(".svg"))
case object Webp extends ImageContentType("image/webp", List(".webp"))

override def values: IndexedSeq[ImageContentType] = findValues
def valueOf(s: String): Option[ImageContentType] = ImageContentType.values.find(_.entryName == s)
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import no.ndla.language.model.WithLanguage
case class ImageFileData(
fileName: String,
size: Long,
contentType: String,
contentType: ImageContentType,
dimensions: Option[ImageDimensions],
variants: Seq[ImageVariant],
override val language: String,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@ import scala.util.Using.Releasable
* The underlying `InputStream` should be closed after being used, either by closing it directly or with
* [[scala.util.Using]]. Failing to close the stream may lead to resource leaks/starvation.
*/
enum ImageStream(val stream: InputStream, val fileName: String, val contentLength: Long, val contentType: String) {
enum ImageStream(
val stream: InputStream,
val fileName: String,
val contentLength: Long,
val contentType: ImageContentType,
) {
case Processable(
override val stream: InputStream,
override val fileName: String,
Expand All @@ -27,13 +32,13 @@ enum ImageStream(val stream: InputStream, val fileName: String, val contentLengt
) extends ImageStream(stream, fileName, contentLength, format.toContentType)

case Gif(override val stream: InputStream, override val fileName: String, override val contentLength: Long)
extends ImageStream(stream, fileName, contentLength, "image/gif")
extends ImageStream(stream, fileName, contentLength, ImageContentType.Gif)

case Unprocessable(
override val stream: InputStream,
override val fileName: String,
override val contentLength: Long,
override val contentType: String,
override val contentType: ImageContentType,
) extends ImageStream(stream, fileName, contentLength, contentType)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ package no.ndla.imageapi.model.domain
import enumeratum.{Enum, EnumEntry}

sealed trait ProcessableImageFormat extends EnumEntry {
def toContentType: String = this match {
case ProcessableImageFormat.Jpeg => "image/jpeg"
case ProcessableImageFormat.Png => "image/png"
case ProcessableImageFormat.Webp => "image/webp"
def toContentType: ImageContentType = this match {
case ProcessableImageFormat.Jpeg => ImageContentType.Jpeg
case ProcessableImageFormat.Png => ImageContentType.Png
case ProcessableImageFormat.Webp => ImageContentType.Webp
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ case class SearchSettings(
widthTo: Option[Int],
heightFrom: Option[Int],
heightTo: Option[Int],
contentType: Option[String],
)
Original file line number Diff line number Diff line change
Expand Up @@ -213,18 +213,18 @@ class ConverterService(using clock: Clock, props: Props) extends StrictLogging {
})
}

def asApiImageTag(domainImageTag: commonDomain.Tag): api.ImageTagDTO = {
private def asApiImageTag(domainImageTag: commonDomain.Tag): api.ImageTagDTO = {
api.ImageTagDTO(domainImageTag.tags, domainImageTag.language)
}

def asApiCaption(domainImageCaption: domain.ImageCaption): api.ImageCaptionDTO =
private def asApiCaption(domainImageCaption: domain.ImageCaption): api.ImageCaptionDTO =
api.ImageCaptionDTO(domainImageCaption.caption, domainImageCaption.language)

def asApiImageTitle(domainImageTitle: domain.ImageTitle): api.ImageTitleDTO = {
private def asApiImageTitle(domainImageTitle: domain.ImageTitle): api.ImageTitleDTO = {
api.ImageTitleDTO(domainImageTitle.title, domainImageTitle.language)
}

def asApiLicense(license: String): commonApi.LicenseDTO = {
private def asApiLicense(license: String): commonApi.LicenseDTO = {
getLicense(license)
.map(l => commonApi.LicenseDTO(l.license.toString, Some(l.description), l.url))
.getOrElse(commonApi.LicenseDTO("unknown", None, None))
Expand Down
Loading