Skip to content

Commit

Permalink
😑
Browse files Browse the repository at this point in the history
  • Loading branch information
fantasticmao committed Sep 6, 2018
1 parent d7c03ac commit 34985d2
Show file tree
Hide file tree
Showing 17 changed files with 558 additions and 0 deletions.
9 changes: 9 additions & 0 deletions .gitignore
@@ -0,0 +1,9 @@
/target/

### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr

.DS_Store
25 changes: 25 additions & 0 deletions README.md
@@ -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
```
Binary file added doc/demo.png
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
71 changes: 71 additions & 0 deletions pom.xml
@@ -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>
23 changes: 23 additions & 0 deletions src/main/java/cn/fantasticmao/util/moment/Args.java
@@ -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;
}
75 changes: 75 additions & 0 deletions src/main/java/cn/fantasticmao/util/moment/FileUtil.java
@@ -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);
}
}
30 changes: 30 additions & 0 deletions src/main/java/cn/fantasticmao/util/moment/ImageType.java
@@ -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);
}
}
42 changes: 42 additions & 0 deletions src/main/java/cn/fantasticmao/util/moment/Main.java
@@ -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 src/main/java/cn/fantasticmao/util/moment/StitchUtil.java
@@ -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);
}

}

0 comments on commit 34985d2

Please sign in to comment.