Read delay between frames in animated GIF - java

How do you read the animated GIF's control block between each frame? I'm interested in the delay between each frame. I've looked at the Javadoc for ImageReader and I'm not seeing anything.
Here's my code for reading all the frames from the animated GIF, how would I enhance it to read the metadata about each frame embedded in the animated GIF?
List<BufferedImage> list = new ArrayList<BufferedImage>();
try {
ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next();
reader.setInput(ImageIO.createImageInputStream(urlImage.openStream()));
int i = reader.getMinIndex();
int numImages = reader.getNumImages(true);
while (i < numImages)
{
list.add(reader.read(i++));
}
// do stuff with frames of image...
} catch (Exception e) {
e.printStackTrace();
}

You will have to seek out "delayTime" attribute from metadata node. Use the following working example to understand:
public class GiffTest {
/**
* #param args the command line arguments
*/
public static void main(String[] args) throws IOException {
ImageReader reader = ImageIO.getImageReadersBySuffix("gif").next();
reader.setInput(ImageIO.createImageInputStream(new FileInputStream("H:\\toonGif.gif")));
int i = reader.getMinIndex();
int numImages = reader.getNumImages(true);
IIOMetadata imageMetaData = reader.getImageMetadata(0);
String metaFormatName = imageMetaData.getNativeMetadataFormatName();
IIOMetadataNode root = (IIOMetadataNode)imageMetaData.getAsTree(metaFormatName);
IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension");
System.out.println(graphicsControlExtensionNode.getAttribute("delayTime"));
}
private static IIOMetadataNode getNode(IIOMetadataNode rootNode, String nodeName) {
int nNodes = rootNode.getLength();
for (int i = 0; i < nNodes; i++) {
if (rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName)== 0) {
return((IIOMetadataNode) rootNode.item(i));
}
}
IIOMetadataNode node = new IIOMetadataNode(nodeName);
rootNode.appendChild(node);
return(node);
}
}

Related

itext split PDF into several PDF but get same size

this is my code for split single PDF in a several PDF splitted by page:
public static String splitAndRenamePdf(InputStream file, String targetDir) {
try {
PdfReader reader = new PdfReader(file);
int n = reader.getNumberOfPages();
for (int i=1; i <= n; i++) {
Document document = new Document(reader.getPageSizeWithRotation(i)); //I tried with 1 too
PdfCopy writer = new PdfCopy(document, new FileOutputStream(targetDir+File.separatorChar+i+".pdf"));
document.open();
PdfImportedPage page = writer.getImportedPage(reader, i);
writer.addPage(page);
document.close();
writer.close();
}
return "from 01 to "+n;
} catch (IOException | DocumentException exc) {
System.out.println("splitAndRenamePdf Exception: "+exc.getMessage());
return null;
}
}
the content is right but the resulting n files are each the same size as the original.
Someone could help me? I could change library because I'm not legacy with iText.
I write the solution...
I hope it could help someone.
private final static RenderListener nopListener = new RenderListener() {
#Override
public void renderText(TextRenderInfo renderInfo) { }
#Override
public void renderImage(ImageRenderInfo renderInfo) { }
#Override
public void endTextBlock() { }
#Override
public void beginTextBlock() { }
};
static class Do implements ContentOperator {
public void invoke(PdfContentStreamProcessor processor, PdfLiteral operator, ArrayList<PdfObject> operands) {
PdfName xobjectName = (PdfName)operands.get(0);
names.add(xobjectName);
}
final List<PdfName> names = new ArrayList<>();
}
private static void fixPdfReader(PdfReader reader) throws IOException {
PdfContentStreamProcessor processor = new PdfContentStreamProcessor(nopListener);
Do doOp = new Do();
processor.registerContentOperator("Do", doOp);
int totPages = reader.getNumberOfPages();
for (int page = 1; page <= totPages; page++) {
PdfDictionary resources = reader.getPageResources(page);
if (resources == null) {
System.out.printf("!!! page %d has no resources\n", page);
continue;
}
doOp.names.clear();
processor.processContent(ContentByteUtils.getContentBytesForPage(reader, page), resources);
PdfDictionary newResources = new PdfDictionary();
newResources.putAll(resources);
PdfDictionary xobjects = newResources.getAsDict(PdfName.XOBJECT);
PdfDictionary newXobjects = new PdfDictionary();
for (PdfName key: doOp.names) {
newXobjects.put(key, xobjects.get(key));
}
newResources.put(PdfName.XOBJECT, newXobjects);
reader.getPageN(page).put(PdfName.RESOURCES, newResources);
}
reader.removeUnusedObjects();
}
public static String fixAndSplitPDF(InputStream inputStream, String targetDir) {
try {
PdfReader reader = new PdfReader(inputStream);
fixPdfReader(reader);
//this method is in the question!
return splitAndRenamePdf(reader, targetDir);
} catch (IOException exc) {
//LOG Exception...
return null;
}
}

Animated GIF: retain transparency without ghosting

In trying to make an animated GIF from an image with transparency this code is producing a ghosting effect from the earlier image frames.
Image of 3rd frame, which shows the first two frames beneath the final frame
How can the code be altered to produce a 'clean' animated image for all frames?
Note: The code uses two methods, written by GeoffTitmus & Maxideon, that were formerly on the Oracle forums site. The links I first saw referenced in the code (in the JavaDocs section) have long since disappeared, so were removed. Other than that, only a few JavaDoc warnings were corrected, otherwise they are exactly as found from around the internet or as used in my older code projects.
import java.awt.*;
import java.awt.image.*;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import java.io.*;
import javax.imageio.*;
import javax.imageio.metadata.IIOInvalidTreeException;
import javax.imageio.stream.*;
import javax.imageio.metadata.*;
import org.w3c.dom.Node;
import java.net.URL;
public class AnimatedGifWithTransparency {
private JComponent ui = null;
public final String PATH = "https://i.stack.imgur.com/P59NF.png";
private BufferedImage image;
AnimatedGifWithTransparency() {
try {
initUI();
} catch (IOException ex) {
ex.printStackTrace();
}
}
/**
* #author GeoffTitmus
* #param out OutputStream A stream in which to store the animation.
* #param frames BufferedImage[] Array of BufferedImages, the frames of the
* animation.
* #param delayTimes String[] Array of Strings, representing the frame delay
* times.
* #throws Exception
*/
public static void saveAnimate(OutputStream out, BufferedImage[] frames, String[] delayTimes) throws Exception {
ImageWriter iw = ImageIO.getImageWritersByFormatName("gif").next();
ImageOutputStream ios = ImageIO.createImageOutputStream(out);
iw.setOutput(ios);
iw.prepareWriteSequence(null);
for (int i = 0; i < frames.length; i++) {
BufferedImage src = frames[i];
ImageWriteParam iwp = iw.getDefaultWriteParam();
IIOMetadata metadata = iw.getDefaultImageMetadata(new ImageTypeSpecifier(src), iwp);
configure(metadata, delayTimes[i], i);
IIOImage ii = new IIOImage(src, null, metadata);
iw.writeToSequence(ii, null);
}
iw.endWriteSequence();
ios.close();
}
/**
* #author Maxideon
* #param meta
* #param delayTime String Frame delay for this frame.
* #param imageIndex
*/
public static void configure(IIOMetadata meta,
String delayTime,
int imageIndex) {
String metaFormat = meta.getNativeMetadataFormatName();
if (!"javax_imageio_gif_image_1.0".equals(metaFormat)) {
throw new IllegalArgumentException(
"Unfamiliar gif metadata format: " + metaFormat);
}
Node root = meta.getAsTree(metaFormat);
//find the GraphicControlExtension node
Node child = root.getFirstChild();
while (child != null) {
if ("GraphicControlExtension".equals(child.getNodeName())) {
break;
}
child = child.getNextSibling();
}
IIOMetadataNode gce = (IIOMetadataNode) child;
gce.setAttribute("userDelay", "FALSE");
gce.setAttribute("delayTime", delayTime);
//only the first node needs the ApplicationExtensions node
if (imageIndex == 0) {
IIOMetadataNode aes
= new IIOMetadataNode("ApplicationExtensions");
IIOMetadataNode ae
= new IIOMetadataNode("ApplicationExtension");
ae.setAttribute("applicationID", "NETSCAPE");
ae.setAttribute("authenticationCode", "2.0");
byte[] uo = new byte[]{
//last two bytes is an unsigned short (little endian) that
//indicates the the number of times to loop.
//0 means loop forever.
0x1, 0x0, 0x0
};
ae.setUserObject(uo);
aes.appendChild(ae);
root.appendChild(aes);
}
try {
meta.setFromTree(metaFormat, root);
} catch (IIOInvalidTreeException e) {
//shouldn't happen
throw new Error(e);
}
}
private BufferedImage getShiftedImage(int frame) {
int w = image.getWidth();
int h = image.getHeight();
BufferedImage bi = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
Graphics g = bi.getGraphics();
int xOffPos = frame*w/3;
int xOffNeg = xOffPos-w;
g.drawImage(image, xOffNeg, 0, ui);
g.drawImage(image, xOffPos, 0, ui);
g.dispose();
return bi;
}
public final void initUI() throws IOException {
if (ui != null) {
return;
}
image = ImageIO.read(new URL(PATH));
ui = new JPanel(new BorderLayout(4, 4));
ui.setBorder(new EmptyBorder(4, 4, 4, 4));
BufferedImage[] frames = {
getShiftedImage(0),
getShiftedImage(1),
getShiftedImage(2)
};
String[] delayTimes = {
"50","50","50"
};
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try {
saveAnimate(baos, frames, delayTimes);
} catch (Exception ex) {
ex.printStackTrace();
return;
}
byte[] bytes = baos.toByteArray();
ui.add(new JLabel(new ImageIcon(bytes)));
}
public JComponent getUI() {
return ui;
}
public static void main(String[] args) {
Runnable r = () -> {
AnimatedGifWithTransparency o = new AnimatedGifWithTransparency();
JFrame f = new JFrame(o.getClass().getSimpleName());
f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
f.setLocationByPlatform(true);
f.setContentPane(o.getUI());
f.pack();
f.setMinimumSize(f.getSize());
f.setVisible(true);
};
SwingUtilities.invokeLater(r);
}
}

Xuggler recording in AVI format

Hi I am trying to use Xuggler for screen recording video in AVI format.but I am getting the following exception while doing the same.couldn't not open the stream.I changed the width and height as well but no use.
Exception in thread "main" java.lang.RuntimeException: could not open stream com.xuggle.xuggler.IStream#4088416[index:0;id:0;streamcoder:com.xuggle.xuggler.IStreamCoder#4088512[codec=com.xuggle.xuggler.ICodec#4088848[type=CODEC_TYPE_VIDEO;id=CODEC_ID_H264;name=libx264;];time base=1/1000000;frame rate=0/0;pixel type=YUV420P;width=683;height=384;];framerate:0/0;timebase:1/90000;direction:OUTBOUND;]: Operation not permitted
at com.xuggle.mediatool.MediaWriter.openStream(MediaWriter.java:1192)
at com.xuggle.mediatool.MediaWriter.getStream(MediaWriter.java:1052)
at com.xuggle.mediatool.MediaWriter.encodeVideo(MediaWriter.java:799)
at com.atmecs.ep.ScreenRecording.main(ScreenRecording.java:52)
public class ScreenRecording {
private static final double FRAME_RATE = 50;
private static final int SECONDS_TO_RUN_FOR = 30;
private static final String outputFilename = "c:/today.avi";
private static Dimension screenBounds;
public static void main(String[] args) {
// let's make a IMediaWriter to write the file.
final IMediaWriter writer = ToolFactory.makeWriter(outputFilename);
screenBounds = Toolkit.getDefaultToolkit().getScreenSize();
// We tell it we're going to add one video stream, with id 0,
// at position 0, and that it will have a fixed frame rate of
// FRAME_RATE.
writer.addListener(ToolFactory.makeDebugListener());
writer.addVideoStream(0, 0, ICodec.ID.CODEC_ID_H264, screenBounds.width / 2, screenBounds.height / 2);
long startTime = System.nanoTime();
for (int index = 0; index < SECONDS_TO_RUN_FOR * FRAME_RATE; index++) {
// take the screen shot
BufferedImage screen = getDesktopScreenshot();
// convert to the right image type
BufferedImage bgrScreen = convertToType(screen, BufferedImage.TYPE_3BYTE_BGR);
// encode the image to stream #0
writer.encodeVideo(0, bgrScreen, System.nanoTime() - startTime, TimeUnit.NANOSECONDS);
// sleep for frame rate milliseconds
try {
Thread.sleep((long) (1000 / FRAME_RATE));
} catch (InterruptedException e) {
// ignore
}
}
// tell the writer to close and write the trailer if needed
writer.close();
}
public static BufferedImage convertToType(BufferedImage sourceImage, int targetType) {
BufferedImage image;
// if the source image is already the target type, return the source
// image
if (sourceImage.getType() == targetType) {
image = sourceImage;
}
// otherwise create a new image of the target type and draw the new
// image
else {
image = new BufferedImage(sourceImage.getWidth(), sourceImage.getHeight(), targetType);
image.getGraphics().drawImage(sourceImage, 0, 0, null);
}
return image;
}
private static BufferedImage getDesktopScreenshot() {
try {
Robot robot = new Robot();
Rectangle captureSize = new Rectangle(screenBounds);
return robot.createScreenCapture(captureSize);
} catch (AWTException e) {
e.printStackTrace();
return null;
}
}
}
can you give any suggestion for encoding the video(only AVI)

How to create an image from first page of a pdf in iText

I want to create image from first page of an PDF . I am using iText in java . Can you suggest me what to do to extract first page of an pdf as an image ?
Document document = new Document();
PdfWriter writer = PdfWriter.getInstance(
document, new FileOutputStream(RESULT));
document.open();
File extStore = Environment.getExternalStorageDirectory();
String path=extStore.getPath()+"/FirstPdf.pdf";
PdfReader reader = new PdfReader(path);
int n = reader.getNumberOfPages();
PdfImportedPage page;
for (int i = 1; i <= n; i++) {
page = writer.getImportedPage(reader, i);
// Image.getInstance(page) ;
}
document.close();
I have written the above code . What to do to extract first page of a pdf as an image and save it in SDCARD ?
iText doesn't work for that purpose.
http://www.java2s.com/Open-Source/Android_Free_Code/Pdf/Download_Free_code_Android_Pdf_Viewer_Library.htm
The jar file is in the zip.
Download that library PdfViewer.jar and try this code:
byte[] bytes;
try {
File file = new File("/storage/extSdCard/Test.pdf");
FileInputStream is = new FileInputStream(file);
// Get the size of the file
long length = file.length();
bytes = new byte[(int) length];
int offset = 0;
int numRead = 0;
while (offset < bytes.length && (numRead=is.read(bytes, offset, bytes.length-offset)) >= 0) {
offset += numRead;
}
ByteBuffer buffer = ByteBuffer.NEW(bytes);
String data = Base64.encodeToString(bytes, Base64.DEFAULT);
PDFFile pdf_file = new PDFFile(buffer);
PDFPage page = pdf_file.getPage(2, true);
RectF rect = new RectF(0, 0, (int) page.getBBox().width(),
(int) page.getBBox().height());
Bitmap image = page.getImage((int)rect.width(), (int)rect.height(), rect);
FileOutputStream os = new FileOutputStream("/storage/extSdCard/pdf.jpg");
image.compress(Bitmap.CompressFormat.JPEG, 80, os);
//((ImageView) findViewById(R.id.testView)).setImageBitmap(image);
} catch (Exception e) {
e.printStackTrace();
}
You can change the rect around to make it extract any part of the pdf you want etc too, pretty good. Spent about 16 hours banging my head against a wall before finding that solution. Wasn't really sure if it was possible without the swing awt library. Sorry the storage is hard coded but it was the least of my concerns at the time.
I ended up finding out how to do what the question initially asked!!!
You need iTextG library (itextg-5.5.3.jar), scpkix-jdk15on.1.47.0.1.jar & scprov-jdk15on-1.47.0.2.jar
inside where want to call it from:
public static final String RESULT = "/storage/sdcard0/dirAtExtStorage/Img%s.%s";
public void extractImages(String filename)
throws IOException, DocumentException {
PdfReader reader = new PdfReader(filename);
PdfReaderContentParser parser = new PdfReaderContentParser(reader);
MyImageRenderListener listener = new MyImageRenderListener(RESULT);
for (int i = 1; i <= reader.getNumberOfPages(); i++) {
parser.processContent(i, listener);
}
}
inside MyImageRendererListener.java:
public class MyImageRenderListener implements RenderListener{
private String path;
public MyImageRenderListener(String path) {
this.path = path;
}
#Override
public void beginTextBlock() {
// TODO Auto-generated method stub
}
#Override
public void endTextBlock() {
// TODO Auto-generated method stub
}
public void renderImage(ImageRenderInfo renderInfo) {
try {
System.out.print("renderImage");
String filename;
FileOutputStream os;
PdfImageObject image = renderInfo.getImage();
if (image == null) return;
filename = String.format(path, renderInfo.getRef().getNumber(), image.getFileType());
os = new FileOutputStream(filename);
os.write(image.getImageAsBytes());
os.flush();
os.close();
} catch (IOException e) {
System.out.println(e.getMessage());
}
}
#Override
public void renderText(TextRenderInfo arg0) {
// TODO Auto-generated method stub
}
}
enjoy guys

GIF animation plays extremely fast in JFrame

import java.awt.image.BufferedImage;
import java.io.IOException;
import java.io.InputStream;
import javax.imageio.ImageIO;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFrame;
import javax.swing.JLabel;
import common.ResourcesToAccess;
public class RecordingStartingThread extends Thread{
#Override
public void run(){
JFrame f = new JFrame();
ImageIcon reel = new ImageIcon("src/images/reel.GIF");
JLabel label = new JLabel(reel);
reel.setImageObserver(label);
f.getContentPane().add(label);
f.setUndecorated(true);
f.setSize(300, 300);
f.setVisible(true);
}
public static void main(String[] args) {
new RecordingStartingThread().start();
}
}
Issue: GIF plays extremely fast.
Question: How do I make sure that GIF plays at a normal speed?
As for GIF speed playback - I've encountered this problem too. If I remember correctly this was caused by the "default" (or not provided?) value for frame rate in GIF file. Some web browsers "overrided" that frame rate so that GIF played correctly.
As a result I created a class that converts GIF (read GIF -> write GIF) and give frame rate provided by the user. com.madgag.gif.fmsware.AnimatedGifEncoder class is an external library that I link to the project via Maven: animated-gif-lib-1.0.jar
public final class GIFUtils {
private GIFUtils() {
}
public static List<BufferedImage> extractFrames(String filePath) throws IOException {
return extractFrames(new File(filePath));
}
public static List<BufferedImage> extractFrames(File file) throws IOException {
List<BufferedImage> imgs = new LinkedList<BufferedImage>();
ImageReader reader = ImageIO.getImageReadersBySuffix("GIF").next();
ImageInputStream in = null;
try {
in = ImageIO.createImageInputStream(new FileInputStream(file));
reader.setInput(in);
BufferedImage img = null;
int count = reader.getNumImages(true);
for(int i = 0; i < count; i++) {
Node tree = reader.getImageMetadata(i).getAsTree("javax_imageio_gif_image_1.0");
int x = Integer.valueOf(tree.getChildNodes().item(0).getAttributes()
.getNamedItem("imageLeftPosition").getNodeValue());
int y = Integer.valueOf(tree.getChildNodes().item(0).getAttributes()
.getNamedItem("imageTopPosition").getNodeValue());
BufferedImage image = reader.read(i);
if(img == null) {
img = new BufferedImage(image.getWidth() + x, image.getHeight() + y,
BufferedImage.TYPE_4BYTE_ABGR);
}
Graphics2D g = img.createGraphics();
ImageUtils.setBestRenderHints(g);
g.drawImage(image, x, y, null);
imgs.add(ImageUtils.copy(img));
}
}
finally {
if(in != null) {
in.close();
}
}
return imgs;
}
public static void writeGif(List<BufferedImage> images, File gifFile, int millisForFrame)
throws FileNotFoundException, IOException {
BufferedImage firstImage = images.get(0);
int type = firstImage.getType();
ImageOutputStream output = new FileImageOutputStream(gifFile);
// create a gif sequence with the type of the first image, 1 second
// between frames, which loops continuously
GifSequenceWriter writer = new GifSequenceWriter(output, type, 100, false);
// write out the first image to our sequence...
writer.writeToSequence(firstImage);
for(int i = 1; i < images.size(); i++) {
BufferedImage nextImage = images.get(i);
writer.writeToSequence(nextImage);
}
writer.close();
output.close();
}
public static Image createGif(List<BufferedImage> images, int millisForFrame) {
AnimatedGifEncoder g = new AnimatedGifEncoder();
ByteArrayOutputStream out = new ByteArrayOutputStream(5 * 1024 * 1024);
g.start(out);
g.setDelay(millisForFrame);
g.setRepeat(1);
for(BufferedImage i : images) {
g.addFrame(i);
}
g.finish();
byte[] bytes = out.toByteArray();
return Toolkit.getDefaultToolkit().createImage(bytes);
}
And GifSequenceWriter looks like this:
public class GifSequenceWriter {
protected ImageWriter gifWriter;
protected ImageWriteParam imageWriteParam;
protected IIOMetadata imageMetaData;
public GifSequenceWriter(ImageOutputStream outputStream, int imageType, int timeBetweenFramesMS,
boolean loopContinuously) throws IIOException, IOException {
gifWriter = getWriter();
imageWriteParam = gifWriter.getDefaultWriteParam();
ImageTypeSpecifier imageTypeSpecifier = ImageTypeSpecifier.createFromBufferedImageType(imageType);
imageMetaData = gifWriter.getDefaultImageMetadata(imageTypeSpecifier, imageWriteParam);
String metaFormatName = imageMetaData.getNativeMetadataFormatName();
IIOMetadataNode root = (IIOMetadataNode) imageMetaData.getAsTree(metaFormatName);
IIOMetadataNode graphicsControlExtensionNode = getNode(root, "GraphicControlExtension");
graphicsControlExtensionNode.setAttribute("disposalMethod", "none");
graphicsControlExtensionNode.setAttribute("userInputFlag", "FALSE");
graphicsControlExtensionNode.setAttribute("transparentColorFlag", "FALSE");
graphicsControlExtensionNode.setAttribute("delayTime", Integer.toString(timeBetweenFramesMS / 10));
graphicsControlExtensionNode.setAttribute("transparentColorIndex", "0");
IIOMetadataNode commentsNode = getNode(root, "CommentExtensions");
commentsNode.setAttribute("CommentExtension", "Created by MAH");
IIOMetadataNode appEntensionsNode = getNode(root, "ApplicationExtensions");
IIOMetadataNode child = new IIOMetadataNode("ApplicationExtension");
child.setAttribute("applicationID", "NETSCAPE");
child.setAttribute("authenticationCode", "2.0");
int loop = loopContinuously ? 0 : 1;
child.setUserObject(new byte[] { 0x1, (byte) (loop & 0xFF), (byte) (loop >> 8 & 0xFF) });
appEntensionsNode.appendChild(child);
imageMetaData.setFromTree(metaFormatName, root);
gifWriter.setOutput(outputStream);
gifWriter.prepareWriteSequence(null);
}
public void writeToSequence(RenderedImage img) throws IOException {
gifWriter.writeToSequence(new IIOImage(img, null, imageMetaData), imageWriteParam);
}
public void close() throws IOException {
gifWriter.endWriteSequence();
}
private static ImageWriter getWriter() throws IIOException {
Iterator<ImageWriter> iter = ImageIO.getImageWritersBySuffix("gif");
if(!iter.hasNext()) {
throw new IIOException("No GIF Image Writers Exist");
}
return iter.next();
}
private static IIOMetadataNode getNode(IIOMetadataNode rootNode, String nodeName) {
int nNodes = rootNode.getLength();
for(int i = 0; i < nNodes; i++) {
if(rootNode.item(i).getNodeName().compareToIgnoreCase(nodeName) == 0) {
return (IIOMetadataNode) rootNode.item(i);
}
}
IIOMetadataNode node = new IIOMetadataNode(nodeName);
rootNode.appendChild(node);
return node;
}
}
The easiest fix for this problem is to just create a .gif file which is "well-formed", i.e. contains the appropriate frame rate. This can be achieved with the help of various online converters, just google "gif speed changer".

Categories