diff --git a/src/main/java/net/lamgc/cgj/pixiv/PixivUgoiraBuilder.java b/src/main/java/net/lamgc/cgj/pixiv/PixivUgoiraBuilder.java new file mode 100644 index 0000000..7e8a2d8 --- /dev/null +++ b/src/main/java/net/lamgc/cgj/pixiv/PixivUgoiraBuilder.java @@ -0,0 +1,180 @@ +package net.lamgc.cgj.pixiv; + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.squareup.gifencoder.GifEncoder; +import com.squareup.gifencoder.Image; +import com.squareup.gifencoder.ImageOptions; +import io.netty.handler.codec.http.HttpHeaderNames; +import org.apache.commons.io.IOUtils; +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.util.EntityUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.imageio.ImageIO; +import java.awt.image.BufferedImage; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Objects; +import java.util.concurrent.TimeUnit; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Pixiv动图构建器. + * 可便捷的接收并处理动图. + */ +public final class PixivUgoiraBuilder { + + private final Logger log = LoggerFactory.getLogger(PixivUgoiraBuilder.class.getSimpleName() + "@" + Integer.toHexString(this.hashCode())); + + private final HttpClient httpClient; + private final JsonObject ugoiraMeta; + private final int illustId; + + private int height; + private int width; + + public PixivUgoiraBuilder(HttpClient httpClient, int illustId) throws IOException { + this.httpClient = Objects.requireNonNull(httpClient); + log.trace("正在获取动图元数据..."); + HttpGet request = new HttpGet(PixivURL.PIXIV_GET_UGOIRA_META_URL.replaceAll("\\{illustId}", String.valueOf(illustId))); + log.debug("Request Url: {}", request.getURI()); + HttpResponse response = httpClient.execute(request); + String bodyStr = EntityUtils.toString(response.getEntity()); + log.debug("JsonBodyStr: {}", bodyStr); + JsonObject resultObject = new Gson().fromJson(bodyStr, JsonObject.class); + if(resultObject.get("error").getAsBoolean()) { + String message = resultObject.get("message").getAsString(); + log.error("获取动图元数据失败!(接口报错: {})", message); + throw new IOException(message); + } else if(!resultObject.has("body")) { + String message = "接口返回数据不存在body属性, 可能接口发生改变!"; + log.error(message); + throw new IOException(message); + } + log.trace("动图元数据获取完成."); + + this.ugoiraMeta = resultObject.getAsJsonObject("body"); + this.illustId = illustId; + } + + /** + * 构造一个动图构建器 + * @param httpClient Http客户端对象 + * @param ugoiraMeta 动图元数据 + */ + public PixivUgoiraBuilder(HttpClient httpClient, JsonObject ugoiraMeta) { + this.httpClient = Objects.requireNonNull(httpClient); + Objects.requireNonNull(ugoiraMeta); + if(!ugoiraMeta.get("error").getAsBoolean() && ugoiraMeta.has("body")) { + this.ugoiraMeta = ugoiraMeta.getAsJsonObject("body"); + } else { + this.ugoiraMeta = ugoiraMeta; + } + String src = this.ugoiraMeta.get("src").getAsString(); + int startIndex = src.lastIndexOf("/"); + illustId = Integer.parseInt(src.substring(startIndex + 1, src.indexOf("_", startIndex))); + log.debug("IllustId: {}, UgoiraMeta: {}", this.illustId, this.ugoiraMeta); + } + + public InputStream buildUgoira(boolean original) throws IOException { + getUgoiraImageSize(); + log.debug("动图尺寸信息: Height: {}, Width: {}", height, width); + + JsonArray frames = ugoiraMeta.getAsJsonArray("frames"); + + log.trace("正在获取帧压缩包..."); + HttpGet request = new HttpGet(ugoiraMeta.get(original ? "originalSrc" : "src").getAsString()); + request.addHeader(HttpHeaderNames.REFERER.toString(), PixivURL.getPixivRefererLink(illustId)); + log.trace("发送请求..."); + HttpResponse response = httpClient.execute(request); + log.trace("请求已发送, 正在处理响应..."); + ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(response.getEntity().getContent(), 64 * 1024)); + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + ZipEntry entry; + ByteArrayOutputStream cacheOutputStream = new ByteArrayOutputStream(512); + HashMap frameMap = new HashMap<>(frames.size()); + while((entry = zipInputStream.getNextEntry()) != null) { + log.trace("ZipEntry {} 正在接收...", entry); + IOUtils.copy(zipInputStream, cacheOutputStream); + frameMap.put(entry.getName(), new ByteArrayInputStream(cacheOutputStream.toByteArray())); + log.trace("ZipEntry {} 已接收完成.", entry); + cacheOutputStream.reset(); + } + + + InputStream firstFrameInput = frameMap.get("000000.jpg"); + BufferedImage firstFrame = ImageIO.read(firstFrameInput); + firstFrameInput.reset(); + if(width != firstFrame.getWidth() || height != firstFrame.getHeight()) { + log.warn("动图第一帧实际尺寸与预设尺寸不符, 将调整尺寸为实际尺寸." + "(差距: Width[{}(预设) -> {}(实际)], Height[{}(预设) -> {}(实际)])", + width, firstFrame.getWidth(), + height, firstFrame.getHeight() + ); + width = firstFrame.getWidth(); + height = firstFrame.getHeight(); + } + + GifEncoder encoder = new GifEncoder(outputStream, width, height, 0); + + frames.forEach(frame -> { + JsonObject frameInfo = frame.getAsJsonObject(); + BufferedImage image; + String frameFileName = frameInfo.get("file").getAsString(); + log.trace("正在插入帧 {}", frameFileName); + try { + image = Objects.requireNonNull(ImageIO.read(frameMap.get(frameFileName))); + int[] rgb = new int[image.getHeight() * image.getWidth()]; + log.debug("FrameName: {}, Height: {}, Width: {}, cacheSize: {}", frameFileName, image.getHeight(), image.getWidth(), rgb.length); + image.getRGB(0, 0, image.getWidth(), image.getHeight(), rgb, 0, image.getWidth()); + encoder.addImage(Image.fromRgb(rgb, image.getWidth()), new ImageOptions().setDelay(frameInfo.get("delay").getAsLong(), TimeUnit.MILLISECONDS)); + } catch (IOException e) { + log.error("解析帧图片数据时发生异常", e); + } + }); + encoder.finishEncoding(); + return new ByteArrayInputStream(outputStream.toByteArray()); + } + + /** + * 获取动图图片大小, 并刷新到构建器内的属性 + */ + private void getUgoiraImageSize() throws IOException { + log.debug("正在从Pixiv获取动图尺寸..."); + HttpGet request = new HttpGet(PixivURL.getPixivIllustInfoAPI(illustId)); + log.debug("Request Url: {}", request.getURI()); + HttpResponse response = httpClient.execute(request); + String responseBody = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8); + log.debug("ResponseBody: {}", responseBody); + JsonObject resultObject = new Gson().fromJson(responseBody, JsonObject.class); + if(resultObject.get("error").getAsBoolean()) { + String message = resultObject.get("message").getAsString(); + log.error("接口返回错误: {}", message); + throw new IOException(message); + } + + JsonArray illustsArray = resultObject.getAsJsonObject("body").getAsJsonArray("illusts"); + if(illustsArray.size() != 1) { + log.error("接口返回空, 查询失败."); + throw new IOException("指定的illustId查询失败"); + } + + JsonObject illustObject = illustsArray.get(0).getAsJsonObject(); + int resultIllustId = illustObject.get("illustId").getAsInt(); + if(resultIllustId != illustId) { + log.error("illustId不符合, 查询失败(原illustId: {}, 返回illustId: {})", illustId, resultIllustId); + throw new IOException("接口返回数据不符合"); + } + + this.height = illustObject.get("height").getAsInt(); + this.width = illustObject.get("width").getAsInt(); + log.debug("动图尺寸获取完成(width: {}, height: {})", width, height); + } + +} diff --git a/src/test/java/net/lamgc/cgj/pixiv/PixivUgoiraBuilderTest.java b/src/test/java/net/lamgc/cgj/pixiv/PixivUgoiraBuilderTest.java new file mode 100644 index 0000000..85af7fb --- /dev/null +++ b/src/test/java/net/lamgc/cgj/pixiv/PixivUgoiraBuilderTest.java @@ -0,0 +1,25 @@ +package net.lamgc.cgj.pixiv; + +import org.apache.http.HttpHost; +import org.apache.http.impl.client.CloseableHttpClient; +import org.apache.http.impl.client.HttpClientBuilder; +import org.bouncycastle.util.io.Streams; +import org.junit.Test; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; + +public class PixivUgoiraBuilderTest { + + @Test + public void buildTest() throws IOException { + File outputFile = new File("./output2.gif"); + CloseableHttpClient httpClient = HttpClientBuilder.create().setProxy(new HttpHost("127.0.0.1", 1001)).build(); + PixivUgoiraBuilder builder = new PixivUgoiraBuilder(httpClient, 80766493); + InputStream inputStream = builder.buildUgoira(true); + Files.write(outputFile.toPath(), Streams.readAll(inputStream)); + } + +}