-
Notifications
You must be signed in to change notification settings - Fork 120
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Allow for cropping transparent true color pngs #1878
Changes from all commits
9924fb8
27cfa30
ec6df22
9b23c16
5147f9b
43310fd
33ecf8f
5081fbe
32a30ab
e8092cc
1ec47ce
cecd6fb
0a02ac5
a0281bf
64fbacd
c9a688d
fe51949
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import com.gu.mediaservice.model.{Asset, Bounds, Dimensions, ImageMetadata} | |
import play.api.libs.concurrent.Execution.Implicits._ | ||
|
||
import scala.concurrent.Future | ||
import scala.sys.process._ | ||
|
||
|
||
case class ExportResult(id: String, masterCrop: Asset, othersizings: List[Asset]) | ||
|
@@ -48,7 +49,7 @@ object ImageOperations { | |
val source = addImage(sourceFile) | ||
|
||
val formatter = format(source)("%[JPEG-Colorspace-Name]") | ||
|
||
for { | ||
output <- runIdentifyCmd(formatter) | ||
colourModel = output.headOption | ||
|
@@ -78,18 +79,19 @@ object ImageOperations { | |
} | ||
|
||
def cropImage(sourceFile: File, bounds: Bounds, qual: Double = 100d, tempDir: File, | ||
iccColourSpace: Option[String], colourModel: Option[String]): Future[File] = { | ||
iccColourSpace: Option[String], colourModel: Option[String], fileType: String): Future[File] = { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you think it's worth using an enum or set of case classes (with sealed trait) for MimeType? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I think that's a good idea. I've added a sealed trait for cropping images now. |
||
for { | ||
outputFile <- createTempFile(s"crop-", ".jpg", tempDir) | ||
cropSource = addImage(sourceFile) | ||
qualified = quality(cropSource)(qual) | ||
corrected = correctColour(qualified)(iccColourSpace, colourModel) | ||
converted = applyOutputProfile(corrected) | ||
stripped = stripMeta(converted) | ||
profiled = applyOutputProfile(stripped) | ||
cropped = crop(profiled)(bounds) | ||
addOutput = addDestImage(cropped)(outputFile) | ||
_ <- runConvertCmd(addOutput) | ||
outputFile <- createTempFile(s"crop-", s".${fileType}", tempDir) | ||
cropSource = addImage(sourceFile) | ||
qualified = quality(cropSource)(qual) | ||
corrected = correctColour(qualified)(iccColourSpace, colourModel) | ||
converted = applyOutputProfile(corrected) | ||
stripped = stripMeta(converted) | ||
profiled = applyOutputProfile(stripped) | ||
cropped = crop(profiled)(bounds) | ||
depthAdjusted = depth(cropped)(8) | ||
addOutput = addDestImage(depthAdjusted)(outputFile) | ||
_ <- runConvertCmd(addOutput) | ||
} | ||
yield outputFile | ||
} | ||
|
@@ -101,9 +103,9 @@ object ImageOperations { | |
).map(_ => sourceFile) | ||
} | ||
|
||
def resizeImage(sourceFile: File, dimensions: Dimensions, qual: Double = 100d, tempDir: File): Future[File] = { | ||
def resizeImage(sourceFile: File, dimensions: Dimensions, qual: Double = 100d, tempDir: File, fileType: String): Future[File] = { | ||
for { | ||
outputFile <- createTempFile(s"resize-", ".jpg", tempDir) | ||
outputFile <- createTempFile(s"resize-", s".${fileType}", tempDir) | ||
resizeSource = addImage(sourceFile) | ||
qualified = quality(resizeSource)(qual) | ||
resized = scale(qualified)(dimensions) | ||
|
@@ -113,6 +115,21 @@ object ImageOperations { | |
yield outputFile | ||
} | ||
|
||
def optimiseImage(resizedFile: File, mediaType: MimeType): File = | ||
|
||
mediaType.name match { | ||
case "image/png" => { | ||
val fileName: String = resizedFile.getAbsolutePath() | ||
|
||
val optimisedImageName: String = fileName.split('.')(0) + "optimised.png" | ||
Seq("pngquant", "--quality", "1-85", fileName, "--output", optimisedImageName).! | ||
|
||
new File(optimisedImageName) | ||
|
||
} | ||
case "image/jpeg" => resizedFile | ||
} | ||
|
||
val thumbUnsharpRadius = 0.5d | ||
val thumbUnsharpSigma = 0.5d | ||
val thumbUnsharpAmount = 0.8d | ||
|
@@ -131,4 +148,20 @@ object ImageOperations { | |
_ <- runConvertCmd(addOutput) | ||
} yield outputFile | ||
} | ||
|
||
sealed trait MimeType { | ||
def name: String | ||
def extension: String | ||
} | ||
|
||
case object Png extends MimeType { | ||
val name = "image/png" | ||
val extension = "png" | ||
} | ||
|
||
case object Jpeg extends MimeType { | ||
val name = "image/jpeg" | ||
val extension = "jpg" | ||
} | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
# cropper | ||
|
||
## Requirements | ||
|
||
You need to install | ||
* pngquant | ||
|
||
Pngquant should be available from /usr/bin/ | ||
|
||
If installing pngquant with brew, create a symlink from /usr/local/bin/pngquant to /urs/bin/pngquant. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -2,13 +2,15 @@ package lib | |
|
||
import java.io.File | ||
|
||
import com.gu.mediaservice.lib.imaging.ImageOperations.MimeType | ||
import com.gu.mediaservice.lib.metadata.FileMetadataHelper | ||
|
||
import scala.concurrent.Future | ||
|
||
import com.gu.mediaservice.model._ | ||
import com.gu.mediaservice.lib.Files | ||
import com.gu.mediaservice.lib.imaging.{ImageOperations, ExportResult} | ||
import scala.sys.process._ | ||
|
||
case object InvalidImage extends Exception("Invalid image cannot be cropped") | ||
case object MissingMimeType extends Exception("Missing mimeType from source API") | ||
|
@@ -21,35 +23,54 @@ object Crops { | |
import scala.concurrent.ExecutionContext.Implicits.global | ||
import Files._ | ||
|
||
def outputFilename(source: SourceImage, bounds: Bounds, outputWidth: Int, isMaster: Boolean = false): String = { | ||
s"${source.id}/${Crop.getCropId(bounds)}/${if(isMaster) "master/" else ""}$outputWidth.jpg" | ||
def outputFilename(source: SourceImage, bounds: Bounds, outputWidth: Int, fileType: String, isMaster: Boolean = false): String = { | ||
s"${source.id}/${Crop.getCropId(bounds)}/${if(isMaster) "master/" else ""}$outputWidth.${fileType}" | ||
} | ||
|
||
def createMasterCrop(apiImage: SourceImage, sourceFile: File, crop: Crop, mediaType: String, colourModel: Option[String]): Future[MasterCrop] = { | ||
def createMasterCrop(apiImage: SourceImage, sourceFile: File, crop: Crop, mediaType: MimeType, colourModel: Option[String], | ||
colourType: String): Future[MasterCrop] = { | ||
|
||
val source = crop.specification | ||
val metadata = apiImage.metadata | ||
val iccColourSpace = FileMetadataHelper.normalisedIccColourSpace(apiImage.fileMetadata) | ||
|
||
for { | ||
strip <- ImageOperations.cropImage(sourceFile, source.bounds, 100d, Config.tempDir, iccColourSpace, colourModel) | ||
file <- ImageOperations.appendMetadata(strip, metadata) | ||
strip <- ImageOperations.cropImage(sourceFile, source.bounds, 100d, Config.tempDir, iccColourSpace, colourModel, mediaType.extension) | ||
file: File <- ImageOperations.appendMetadata(strip, metadata) | ||
|
||
|
||
//Before apps and frontend can handle PNG24s we need to pngquant PNG24 master crops | ||
optimisedFile = if (colourType == "True Color with Alpha") { | ||
|
||
val fileName = file.getAbsolutePath() | ||
|
||
|
||
val optimisedImageName: String = fileName.split('.')(0) + "optimised.png" | ||
Seq("pngquant", "--quality", "1-85", fileName, "--output", optimisedImageName).! | ||
new File(optimisedImageName) | ||
} else file | ||
|
||
dimensions = Dimensions(source.bounds.width, source.bounds.height) | ||
filename = outputFilename(apiImage, source.bounds, dimensions.width, true) | ||
sizing = CropStore.storeCropSizing(file, filename, mediaType, crop, dimensions) | ||
filename = outputFilename(apiImage, source.bounds, dimensions.width, mediaType.extension, true) | ||
sizing = CropStore.storeCropSizing(file, filename, mediaType.name, crop, dimensions) | ||
dirtyAspect = source.bounds.width.toFloat / source.bounds.height | ||
aspect = crop.specification.aspectRatio.flatMap(AspectRatio.clean(_)).getOrElse(dirtyAspect) | ||
|
||
} | ||
yield MasterCrop(sizing, file, dimensions, aspect) | ||
yield MasterCrop(sizing, optimisedFile, dimensions, aspect) | ||
} | ||
|
||
def createCrops(sourceFile: File, dimensionList: List[Dimensions], apiImage: SourceImage, crop: Crop, mediaType: String): Future[List[Asset]] = { | ||
def createCrops(sourceFile: File, dimensionList: List[Dimensions], apiImage: SourceImage, crop: Crop, | ||
mediaType: MimeType): Future[List[Asset]] = { | ||
|
||
Future.sequence[Asset, List](dimensionList.map { dimensions => | ||
for { | ||
file <- ImageOperations.resizeImage(sourceFile, dimensions, 75d, Config.tempDir) | ||
filename = outputFilename(apiImage, crop.specification.bounds, dimensions.width) | ||
sizing <- CropStore.storeCropSizing(file, filename, mediaType, crop, dimensions) | ||
_ <- delete(file) | ||
file <- ImageOperations.resizeImage(sourceFile, dimensions, 75d, Config.tempDir, mediaType.extension) | ||
optimisedFile = ImageOperations.optimiseImage(file, mediaType) | ||
filename = outputFilename(apiImage, crop.specification.bounds, dimensions.width, mediaType.extension) | ||
sizing <- CropStore.storeCropSizing(optimisedFile, filename, mediaType.extension, crop, dimensions) | ||
_ <- delete(file) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. dont we just need to delete optimisedFile? (cus it just equals file if it's not one of the special png types) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we probably have to delete both if they exist. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
_ <- delete(optimisedFile) | ||
} | ||
yield sizing | ||
}) | ||
|
@@ -75,12 +96,17 @@ object Crops { | |
val source = crop.specification | ||
val mediaType = apiImage.source.mimeType.getOrElse(throw MissingMimeType) | ||
val secureUrl = apiImage.source.secureUrl.getOrElse(throw MissingSecureSourceUrl) | ||
val cropType = "image/jpeg" | ||
val colourType = apiImage.fileMetadata.colourModelInformation.get("colorType").getOrElse("") | ||
|
||
val cropType = if (mediaType == "image/png" && colourType != "True Color") | ||
ImageOperations.Png | ||
else | ||
ImageOperations.Jpeg | ||
|
||
for { | ||
sourceFile <- tempFileFromURL(secureUrl, "cropSource", "", Config.tempDir) | ||
colourModel <- ImageOperations.identifyColourModel(sourceFile, mediaType) | ||
masterCrop <- createMasterCrop(apiImage, sourceFile, crop, cropType, colourModel) | ||
masterCrop <- createMasterCrop(apiImage, sourceFile, crop, cropType, colourModel, colourType) | ||
|
||
outputDims = dimensionsFromConfig(source.bounds, masterCrop.aspectRatio) :+ masterCrop.dimensions | ||
|
||
|
@@ -91,5 +117,4 @@ object Crops { | |
} | ||
yield ExportResult(apiImage.id, masterSize, sizes) | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Might be better to identify png specifically here and fail in the "other" case.