-
Notifications
You must be signed in to change notification settings - Fork 10
Adding support for animated gifs #13
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
base: master
Are you sure you want to change the base?
Changes from all commits
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 |
---|---|---|
@@ -0,0 +1,67 @@ | ||
package com.rdio.thor | ||
|
||
import com.sksamuel.scrimage.Image | ||
import com.sksamuel.scrimage.io.ImageWriter | ||
import com.twitter.logging.Logger | ||
import java.io.OutputStream | ||
import java.awt.image.BufferedImage | ||
import javax.imageio.{ IIOImage, ImageTypeSpecifier, ImageWriteParam, ImageIO } | ||
import javax.imageio.metadata.IIOMetadataNode | ||
import org.apache.commons.io.IOUtils | ||
import javax.imageio.stream.MemoryCacheImageOutputStream | ||
|
||
case class Frame(image: Image, durationInMs: Int) | ||
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. \n after case class. |
||
class AnimatedGifWriter(frames: List[Frame]) extends ImageWriter { | ||
|
||
|
||
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. Remove extra \n |
||
protected lazy val log = Logger.get(this.getClass) | ||
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. Should remove all logging from utility classes. EDIT: To clarify, only application-level code should log things. |
||
|
||
def write(out: OutputStream) { | ||
val writer = ImageIO.getImageWritersByFormatName("gif").next() | ||
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. Hah, that is a handy function. |
||
val params = writer.getDefaultWriteParam | ||
// TODO : OH SHIT!! What is our BufferedImage type?!?! | ||
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 don't know what this means but it sounds bad. Should be fixed before merging. |
||
|
||
val metaData = writer.getDefaultImageMetadata(ImageTypeSpecifier.createFromBufferedImageType(BufferedImage.TYPE_INT_ARGB), params) | ||
val root:IIOMetadataNode = new IIOMetadataNode(metaData.getNativeMetadataFormatName()) | ||
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. All of the types need to be like |
||
|
||
val graphicsControlExtensionNode:IIOMetadataNode = new IIOMetadataNode("GraphicControlExtension") | ||
graphicsControlExtensionNode.setAttribute("disposalMethod", "none") | ||
graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE") | ||
graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE") | ||
graphicsControlExtensionNode.setAttribute("delayTime", (10).toString) // stupid! "Delay Time (1/100ths of a second)" | ||
graphicsControlExtensionNode.setAttribute("transparentColorIndex", "0") // somewhere else has it as 255 | ||
root.appendChild(graphicsControlExtensionNode) | ||
|
||
val appEntensionsNode:IIOMetadataNode = new IIOMetadataNode("ApplicationExtensions") | ||
val child:IIOMetadataNode = new IIOMetadataNode("ApplicationExtension") | ||
child.setAttribute("applicationID", "NETSCAPE") | ||
child.setAttribute("authenticationCode", "2.0") | ||
val byteArray:Array[Byte] = Array(0x01, 0x00, 0x00) // The last two bytes is the unsigned short (little endian) that represents the number of times to loop. 0 means loop forever. | ||
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. Its just my OCD but the different comment styles makes me sad: Single space on either side of
Double-space before
Double-space on both sides:
|
||
child.setUserObject(byteArray) | ||
appEntensionsNode.appendChild(child) | ||
root.appendChild(appEntensionsNode) | ||
|
||
metaData.mergeTree(metaData.getNativeMetadataFormatName(), root) | ||
|
||
val output = new MemoryCacheImageOutputStream(out) | ||
writer.setOutput(output) | ||
writer.prepareWriteSequence(null) | ||
frames.zipWithIndex foreach { | ||
case (frame:Frame, i) => { | ||
val count = frame.durationInMs | ||
graphicsControlExtensionNode.setAttribute("delayTime", (count / 10).toString) // stupid! "Delay Time (1/100ths of a second)" | ||
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. Should we make a
|
||
root.appendChild(graphicsControlExtensionNode) | ||
metaData.mergeTree(metaData.getNativeMetadataFormatName(), root) | ||
writer.writeToSequence(new IIOImage(frame.image.awt, null, metaData), params) | ||
} | ||
} | ||
writer.endWriteSequence() | ||
writer.dispose() | ||
output.close() | ||
IOUtils.closeQuietly(out) | ||
} | ||
} | ||
|
||
object AnimatedGifWriter { | ||
def apply(frames: List[Frame]): AnimatedGifWriter = new AnimatedGifWriter(frames) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,13 +47,21 @@ abstract class BaseImageService(conf: Config) extends Service[Request, Response] | |
.name("thor-client") | ||
.build() | ||
|
||
def buildAnimatedResponse[T <: ImageWriter](req: Request, frames: List[Frame], format: Format[T], compression: Int = 98): Response = { | ||
val bytes = AnimatedGifWriter(frames).write() | ||
furtherBuildOutResponse(req, bytes, format, compression) | ||
} | ||
|
||
def buildResponse[T <: ImageWriter](req: Request, image: Image, format: Format[T], compression: Int = 98): Response = { | ||
val bytes = format match { | ||
case Format.JPEG => image.writer(format).withCompression(compression).withProgressive(true).write() | ||
case Format.PNG => image.writer(format).withMaxCompression.write() | ||
case Format.GIF => image.writer(format).withProgressive(true).write() | ||
} | ||
furtherBuildOutResponse(req, bytes, format, compression) | ||
} | ||
|
||
def furtherBuildOutResponse[T <: ImageWriter](req: Request, bytes: Array[Byte], format: Format[T], compression: Int = 98): Response = { | ||
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. Hmm, not a fan of this name. Can it be |
||
val expires: Calendar = Calendar.getInstance() | ||
expires.add(Calendar.YEAR, 1) | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,6 +22,7 @@ import com.typesafe.config.Config | |
import org.jboss.netty.handler.codec.http._ | ||
import org.jboss.netty.buffer.ChannelBuffers | ||
|
||
|
||
/** ImageService serves images optionally filtered and blended. */ | ||
class ImageService(conf: Config) extends BaseImageService(conf) { | ||
|
||
|
@@ -179,19 +180,24 @@ class ImageService(conf: Config) extends BaseImageService(conf) { | |
case _: NoopNode => Some(image) | ||
} | ||
} | ||
|
||
def applyLayerFilters(imageMap: Map[String, Image], layers: List[LayerNode], width: Int, height: Int): Option[Image] = { | ||
def applyLayerFilters(imageMap: Map[String, Image], layers: List[LayerNode], width: Int, height: Int): (Option[Image], List[Frame]) = { | ||
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. This will break RdioThor. I don't think we should modify a generic function we use for other things to explicitly include frames. Can we separate the two somehow without much duplication? |
||
// Apply each layer in order | ||
val completedLayers = ArrayBuffer.empty[Image] | ||
var completedFrames:List[Frame] = List() | ||
layers foreach { | ||
case LayerNode(path: ImageNode, filter: FilterNode) => { | ||
tryGetImage(path, imageMap, completedLayers.toArray, width, height) match { | ||
case Some(baseImage) => { | ||
applyFilter(baseImage, filter, imageMap, completedLayers.toArray, width, height) match { | ||
case Some(filteredImage) => completedLayers += filteredImage | ||
case None => { | ||
log.error(s"Failed to apply layer filter: $path $filter") | ||
None | ||
filter match { | ||
case frame:FrameNode => completedFrames = completedFrames :+ Frame(baseImage, frame.durationInMs) | ||
case _ => { | ||
applyFilter(baseImage, filter, imageMap, completedLayers.toArray, width, height) match { | ||
case Some(filteredImage) => completedLayers += filteredImage | ||
case None => { | ||
log.error(s"Failed to apply layer filter: $path $filter") | ||
None | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
@@ -202,7 +208,8 @@ class ImageService(conf: Config) extends BaseImageService(conf) { | |
} | ||
} | ||
} | ||
completedLayers.lastOption | ||
log.info(s" Yo rebecca i see ${completedFrames.length}") | ||
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. Remove :) |
||
(completedLayers.lastOption, completedFrames) | ||
} | ||
|
||
def scaleTo(image: Image, width: Int, height: Int): Image = { | ||
|
@@ -226,6 +233,7 @@ class ImageService(conf: Config) extends BaseImageService(conf) { | |
val compression: Int = math.min(math.max(req.params.getIntOrElse("c", 98), 0), 100) | ||
|
||
val format: Format[ImageWriter] = req.params.get("f") match { | ||
case Some("gif") => Format.GIF.asInstanceOf[Format[ImageWriter]] | ||
case Some("png") => Format.PNG.asInstanceOf[Format[ImageWriter]] | ||
case _ => Format.JPEG.asInstanceOf[Format[ImageWriter]] | ||
} | ||
|
@@ -257,11 +265,16 @@ class ImageService(conf: Config) extends BaseImageService(conf) { | |
|
||
// Apply any filters to each image and return the final image | ||
applyLayerFilters(imageMap, layers, width, height) match { | ||
case Some(image) => { | ||
case (_, frames) if frames.length > 1 => { | ||
// Apply final resize and build response | ||
buildResponse(req, scaleTo(image, width, height), format, compression) | ||
val resizedFrames = frames.map( f => Frame(scaleTo(f.image, width, height), f.durationInMs)) | ||
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.
|
||
buildAnimatedResponse(req, resizedFrames, format, compression) | ||
} | ||
case None => { | ||
case (Some(image), _) => { | ||
// Apply final resize and build response | ||
buildResponse(req, image, format, compression) | ||
} | ||
case _ => { | ||
Response(HttpVersion.HTTP_1_1, HttpResponseStatus.NOT_FOUND) | ||
} | ||
} | ||
|
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.
Remove spaces inside the
{}
.