-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d7c03ac
commit 34985d2
Showing
17 changed files
with
558 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
/target/ | ||
|
||
### IntelliJ IDEA ### | ||
.idea | ||
*.iws | ||
*.iml | ||
*.ipr | ||
|
||
.DS_Store |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,25 @@ | ||
# Moment [![image](https://img.shields.io/badge/license-MIT-green.svg)](https://github.com/FantasticMao/moment/blob/master/LICENSE) [![image](https://img.shields.io/badge/release-download-blue.svg)](https://github.com/FantasticMao/moment/releases) | ||
|
||
Moment 是一个简单(可能没用)的小工具,它可以把多张图片(一般是视频截图)的底部字幕,拼接到第一张图片底下。 | ||
|
||
## 在线演示 | ||
Moment 可以用来保存视频的精彩瞬间: | ||
|
||
[![demo](doc/demo.png)](https://www.bilibili.com/video/av31172471) | ||
|
||
## 下载和使用 | ||
Moment 下载地址:https://github.com/FantasticMao/moment/releases ,它以 Java 语言编写,以 .jar 文件发布,使用时需预先 [安装 Java 运行环境](https://www.baidu.com/s?wd=安装%20JRE)。 | ||
|
||
Moment 使用手册: | ||
``` | ||
java -jar moment-1.0.jar [options] | ||
Options: | ||
* -p, --path | ||
The path of image list that need to be stitched | ||
-h, --height | ||
The bottom subtitle height should be in unit px | ||
Default: 120 | ||
--out | ||
The directory to save the finished image | ||
Default: the current directory | ||
``` |
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,71 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
|
||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<groupId>cn.fantasticmao.util</groupId> | ||
<artifactId>moment</artifactId> | ||
<version>1.0</version> | ||
|
||
<name>moment</name> | ||
<url>https://github.com/FantasticMao/moment</url> | ||
|
||
<properties> | ||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> | ||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> | ||
<java.version>1.8</java.version> | ||
</properties> | ||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>junit</groupId> | ||
<artifactId>junit</artifactId> | ||
<version>4.11</version> | ||
<scope>test</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.projectlombok</groupId> | ||
<artifactId>lombok</artifactId> | ||
<version>1.18.2</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>com.beust</groupId> | ||
<artifactId>jcommander</artifactId> | ||
<version>1.72</version> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<!-- http://maven.apache.org/plugins/maven-assembly-plugin/ --> | ||
<plugin> | ||
<artifactId>maven-assembly-plugin</artifactId> | ||
<version>3.1.0</version> | ||
<configuration> | ||
<descriptorRefs> | ||
<descriptorRef>jar-with-dependencies</descriptorRef> | ||
</descriptorRefs> | ||
|
||
<archive> | ||
<manifest> | ||
<mainClass>cn.fantasticmao.util.moment.Main</mainClass> | ||
</manifest> | ||
</archive> | ||
</configuration> | ||
<executions> | ||
<execution> | ||
<id>make-assembly</id> | ||
<phase>package</phase> | ||
<goals> | ||
<goal>single</goal> | ||
</goals> | ||
</execution> | ||
</executions> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
package cn.fantasticmao.util.moment; | ||
|
||
import com.beust.jcommander.Parameter; | ||
import lombok.Data; | ||
|
||
import java.io.File; | ||
import java.util.List; | ||
|
||
/** | ||
* Args 命令行输入参数 | ||
* | ||
* @author maodh | ||
* @since 2018/9/4 | ||
*/ | ||
@Data | ||
class Args { | ||
@Parameter(names = {"-p", "--path"}, description = "The path of image list that need to be stitched", required = true, order = 0) | ||
private List<String> pathList; | ||
@Parameter(names = {"-h", "--height"}, description = "The bottom subtitle height should be in unit px", order = 1) | ||
private Integer height = 120; | ||
@Parameter(names = "--out", description = "The directory to save the finished image", order = 2) | ||
private String outputDir = System.getProperty("user.dir") + File.separator; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,75 @@ | ||
package cn.fantasticmao.util.moment; | ||
|
||
import java.io.File; | ||
import java.io.FileInputStream; | ||
import java.io.IOException; | ||
|
||
/** | ||
* FileUtil | ||
* | ||
* @author maodh | ||
* @since 2018/9/4 | ||
*/ | ||
class FileUtil { | ||
|
||
// common file utils | ||
|
||
static String getFileNameWithoutExtension(File file) { | ||
String fileName = file.getName(); | ||
int lastIndexOf = fileName.lastIndexOf("."); | ||
return lastIndexOf == -1 ? "" : fileName.substring(0, lastIndexOf); | ||
} | ||
|
||
static String getFileExtension(File file) { | ||
String fileName = file.getName(); | ||
int lastIndexOf = fileName.lastIndexOf("."); | ||
return lastIndexOf == -1 ? "" : fileName.substring(lastIndexOf); | ||
} | ||
|
||
static String getFileExtensionWithoutDot(File file) { | ||
String fileName = file.getName(); | ||
int lastIndexOf = fileName.lastIndexOf("."); | ||
return lastIndexOf == -1 ? "" : fileName.substring(lastIndexOf + 1); | ||
} | ||
|
||
// image file utils | ||
|
||
/** | ||
* 比较图片的后缀名和魔数是否相符 | ||
* | ||
* @see cn.fantasticmao.util.moment.ImageType | ||
*/ | ||
static boolean compareImageExtensionAndMagicNumber(File image) { | ||
// 图片的后缀名 | ||
final String imageExtension = FileUtil.getFileExtension(image); | ||
// 图片的类型 | ||
final ImageType imageType = ImageType.ofExtension(imageExtension); | ||
// 图片的魔数 | ||
final String imageMagicNumber = FileUtil.getImageMagicNumber(image); | ||
return imageType != null && imageMagicNumber.startsWith(imageType.magicNumber); | ||
} | ||
|
||
private static String getImageMagicNumber(File file) { | ||
final int byteSize = 1 << 2; | ||
byte[] bytes = new byte[byteSize]; | ||
try (FileInputStream in = new FileInputStream(file)) { | ||
in.read(bytes, 0, byteSize); | ||
return bytesToHex(bytes); | ||
} catch (IOException e) { | ||
e.printStackTrace(); | ||
return ""; | ||
} | ||
} | ||
|
||
private static final char[] HEX_ARRAY = "0123456789ABCDEF".toCharArray(); | ||
|
||
private static String bytesToHex(byte[] bytes) { | ||
char[] hexChars = new char[bytes.length * 2]; | ||
for (int j = 0; j < bytes.length; j++) { | ||
int v = bytes[j] & 0xFF; | ||
hexChars[j * 2] = HEX_ARRAY[v >>> 4]; | ||
hexChars[j * 2 + 1] = HEX_ARRAY[v & 0x0F]; | ||
} | ||
return new String(hexChars); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
package cn.fantasticmao.util.moment; | ||
|
||
import java.util.stream.Stream; | ||
|
||
/** | ||
* ImageType | ||
* | ||
* @author maodh | ||
* @since 2018/9/5 | ||
*/ | ||
enum ImageType { | ||
BMP(".bmp", "424D"), | ||
JPG(".jpg", "FFD8FFDB"), | ||
JPEG(".jpeg", "FFD8FF"), | ||
PNG(".png", "89504E47"); | ||
|
||
public final String extension; | ||
public final String magicNumber; | ||
|
||
ImageType(String extension, String magicNumber) { | ||
this.extension = extension; | ||
this.magicNumber = magicNumber; | ||
} | ||
|
||
public static ImageType ofExtension(final String extension) { | ||
return Stream.of(ImageType.values()) | ||
.filter(fileType -> extension.equals(fileType.extension)) | ||
.findFirst().orElse(null); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
package cn.fantasticmao.util.moment; | ||
|
||
import com.beust.jcommander.JCommander; | ||
import com.beust.jcommander.ParameterException; | ||
|
||
import java.io.File; | ||
import java.util.List; | ||
import java.util.stream.Collectors; | ||
|
||
/** | ||
* Main | ||
* | ||
* @author maodh | ||
* @since 2018/9/4 | ||
*/ | ||
public class Main { | ||
|
||
public static void main(String[] args) { | ||
Args arguments = new Args(); | ||
try { | ||
JCommander jCommander = JCommander.newBuilder().addObject(arguments).build(); | ||
jCommander.parse(args); | ||
} catch (ParameterException e) { | ||
e.usage(); | ||
return; | ||
} | ||
|
||
final List<File> imageFileList = arguments.getPathList().stream().map(File::new).collect(Collectors.toList()); | ||
final int subtitleHeight = arguments.getHeight(); | ||
final String outputDir = arguments.getOutputDir(); | ||
|
||
try { | ||
File finalImageFile = StitchUtil.stitchImageList(imageFileList, subtitleHeight, outputDir); | ||
System.out.println("well done, you can find your image here [ " + finalImageFile.getPath() + " ]"); | ||
} catch (IllegalArgumentException e) { | ||
System.out.println("failed, illegal argument error suggestion: [ " + e.getMessage() + " ]"); | ||
} catch (Exception e) { | ||
System.out.println("failed, report the problem mailto:maomao8017@gmail.com"); | ||
e.printStackTrace(); | ||
} | ||
} | ||
} |
108 changes: 108 additions & 0 deletions
108
src/main/java/cn/fantasticmao/util/moment/StitchUtil.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
package cn.fantasticmao.util.moment; | ||
|
||
import javax.imageio.ImageIO; | ||
import java.awt.image.BufferedImage; | ||
import java.io.File; | ||
import java.io.IOException; | ||
import java.util.List; | ||
import java.util.Objects; | ||
|
||
/** | ||
* StitchUtil | ||
* | ||
* @author maodh | ||
* @since 2018/9/4 | ||
*/ | ||
class StitchUtil { | ||
|
||
/** | ||
* @param imageFileList 图片文件列表 | ||
* @param subtitleHeight 字幕高度,单位 px | ||
* @param outputDir 保存最终图片文件的目录 | ||
* @return 拼接而成的最终图片文件 | ||
* @throws IOException I/O Exception | ||
*/ | ||
static File stitchImageList(List<File> imageFileList, final int subtitleHeight, final String outputDir) throws IOException { | ||
// 校验参数 | ||
if (imageFileList == null || imageFileList.size() == 0) { | ||
throw new IllegalArgumentException("the image list should not be empty"); | ||
} else { | ||
if (imageFileList.stream().anyMatch(file -> !file.exists())) { | ||
throw new IllegalArgumentException("every list should be filled with image"); | ||
} | ||
} | ||
if (subtitleHeight <= 0) { | ||
throw new IllegalArgumentException("the subtitle height should be a positive integer"); | ||
} | ||
if (outputDir == null || outputDir.length() == 0) { | ||
throw new IllegalArgumentException("the output directory path should not be empty"); | ||
} else { | ||
File outputFile = new File(outputDir); | ||
if (!outputFile.exists() || !outputFile.isDirectory()) { | ||
throw new IllegalArgumentException("there must be an output directory for finished image"); | ||
} | ||
} | ||
|
||
final File firstImageFile = imageFileList.get(0); | ||
// 校验第一张图片文件的后缀名和魔数 | ||
if (!FileUtil.compareImageExtensionAndMagicNumber(firstImageFile)) { | ||
throw new IllegalArgumentException("the format of No.1 image is not supported or cannot opened"); | ||
} | ||
|
||
// 第一张图片的后缀名 | ||
final String firstImageFileExtensionWithoutDot = FileUtil.getFileExtensionWithoutDot(firstImageFile); | ||
|
||
// 最终拼接而成的图片 | ||
BufferedImage firstImage = ImageIO.read(firstImageFile); | ||
final int finalImageWidth = firstImage.getWidth(); | ||
final int finalImageHeight = firstImage.getHeight() + (imageFileList.size() - 1) * subtitleHeight; | ||
final int finalImageType = firstImage.getType(); | ||
BufferedImage finalImage = new BufferedImage(finalImageWidth, finalImageHeight, finalImageType); | ||
|
||
// 拼接图片 | ||
finalImage.createGraphics().drawImage(firstImage, 0, 0, null); | ||
for (int i = 0; i < imageFileList.size(); i++) { | ||
if (i == 0) continue; | ||
File thisImageFile = imageFileList.get(i); | ||
|
||
// 校验当前图片文件的后缀名和魔数 | ||
if (!FileUtil.compareImageExtensionAndMagicNumber(thisImageFile)) { | ||
throw new IllegalArgumentException("the format of No." + (i + 1) + " image is not supported or cannot opened"); | ||
} | ||
|
||
// 校验当前图片文件的后缀名 | ||
final String thisImageFileExtensionWithoutDot = FileUtil.getFileExtensionWithoutDot(thisImageFile); | ||
if (!Objects.equals(thisImageFileExtensionWithoutDot, firstImageFileExtensionWithoutDot)) { | ||
throw new IllegalArgumentException("the extension of the No." + (i + 1) + " image should be same as the first image"); | ||
} | ||
|
||
BufferedImage thisImage = ImageIO.read(thisImageFile); | ||
|
||
// 校验当前图片的宽度 | ||
if (thisImage.getWidth() != firstImage.getWidth()) { | ||
throw new IllegalArgumentException("the width of the No." + (i + 1) + " image should be same as the first image"); | ||
} | ||
|
||
// 裁剪图片字幕 | ||
BufferedImage subtitle = thisImage.getSubimage(0, thisImage.getHeight() - subtitleHeight, thisImage.getWidth(), subtitleHeight); | ||
|
||
// 拼接图片字幕 | ||
finalImage.createGraphics().drawImage(subtitle, 0, firstImage.getHeight() + (i - 1) * subtitleHeight, null); | ||
} | ||
|
||
// 如果最终图片文件已存在,则会覆盖 | ||
File finalImageFile = newFinalImageFileFromImageList(imageFileList, outputDir); | ||
ImageIO.write(finalImage, firstImageFileExtensionWithoutDot, finalImageFile); | ||
return finalImageFile; | ||
} | ||
|
||
private static File newFinalImageFileFromImageList(List<File> imageFileList, String outputDir) { | ||
final File firstImageFile = imageFileList.get(0); | ||
final String finalImageFileNameWithoutExtension = FileUtil.getFileNameWithoutExtension(firstImageFile); | ||
final String finalImageFileExtension = FileUtil.getFileExtension(firstImageFile); | ||
|
||
final String finalImagePath = outputDir + finalImageFileNameWithoutExtension + "_final" + finalImageFileExtension; | ||
return new File(finalImagePath); | ||
} | ||
|
||
} |
Oops, something went wrong.