精灵图(雪碧图)的生成和使用

前言

一些短视频播放器在快进的时候经常能看到下方进度条展示的一个小的的单帧图片,亚马逊的转码服务(AWS MediaConvert)通过配置单帧缩略图来生成多个单帧图片。前端处理的时候需要按照时间每10秒去请求一次图片。

配置方式:

优化

由于是缩略图,没有必要多次请求,因此考虑使用lambda拼接成一张精灵图,前端根据坐标读取即可。

后端生成精灵图:

java 复制代码
package req;

import lombok.Data;
import pers.cz.swiss.JsonUtils;

import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * @version 1.0
 * 精灵图的核心逻辑是将多个图片合成为一个大图片,同时记录每一张图片在拼合后的大图中的位置
 * @description: 精灵图测试
 * @author: ChengZhi 0336
 * @create: 2025-10-09 16:47
 **/
public class SpriteImg {

	public static void main(String[] args) throws IOException {

		// 读取图片
		String dir = "C:\\Users\\chengzhi\\Desktop\\视频封面";
		File dirFile = new File(dir);
		File[] files = dirFile.listFiles();
		// 创建新图片
		File newFile = new File("C:\\Users\\chengzhi\\Desktop\\视频封面\\new.png");
		BufferedImage bufferedImage = ImageUtils.mergeImages(files, true);

		ImageIO.write(bufferedImage, "png", newFile);
	}

}

@Data
class Location {

	private int x;
	private int y;
	private int width;
	private int height;
	private String name;

}

class ImageUtils {

	/**
	 * 合并图片, 支持横向或竖向合并
	 * @param images
	 * @param isHorizontal
	 * @return
	 * @throws IOException
	 */
	public static BufferedImage mergeImages(File[] images, boolean isHorizontal) throws IOException {

		int imagesCount = images.length;
		System.out.println("待合并总数:" + imagesCount);
		int maxWidth = 0;
		int maxHeight = 0;
		Map<String, BufferedImage> bufferedImages = new HashMap<>(imagesCount);
		for (File image : images) {

			BufferedImage bufferedImage = ImageIO.read(image);
			if (bufferedImage == null) {
				continue;
			}
			int width = bufferedImage.getWidth();
			int height = bufferedImage.getHeight();

			if (isHorizontal) {
				maxHeight = Math.max(maxHeight, height);
				maxWidth += width;
			} else {
				maxWidth = Math.max(maxWidth, width);
				maxHeight += height;
			}

			bufferedImages.put(image.getName(), bufferedImage);
		}

		BufferedImage finalBufferedImage = new BufferedImage(maxWidth, maxHeight, BufferedImage.TYPE_INT_ARGB);

		int startX = 0;
		int startY = 0;
		Map<String, Location> map = new HashMap<>();
		for (Map.Entry<String, BufferedImage> entry : bufferedImages.entrySet()) {
			BufferedImage bufImage = entry.getValue();
			int width = bufImage.getWidth();
			int height = bufImage.getHeight();
			int[] ImageArray = new int[width * height];;
			int[] rgb = bufImage.getRGB(0, 0, width, height, ImageArray, 0, width);
			Location location = new Location();
			location.setWidth(width);
			location.setHeight(height);
			location.setX(startX);
			location.setY(startY);
			if (isHorizontal) {
				finalBufferedImage.setRGB(startX, startY, width, height, rgb, 0, width);
				startX += width;
			} else {
				finalBufferedImage.setRGB(startX, startY, width, height, rgb, 0, width);
				startY += height;
			}
			map.put(entry.getKey(), location);
		}

		System.out.println("合并完成!");
		System.out.println("图片位置:" + JsonUtils.toJson(map));

		return finalBufferedImage;
	}

}

前端展示demo:

js 复制代码
<script setup>
import {ref, computed} from "vue";
import sprite from "@/views/drama/new.png"
import axios from "axios";

const image = ref(sprite)


// 精灵图元数据
const sprites = {"穿条纹睡衣的男孩.jpg":{"x":0,"y":0,"width":540,"height":796},"蝴蝶效应.jpg":{"x":540,"y":0,"width":540,"height":799},"天空之城.jpg":{"x":1080,"y":0,"width":540,"height":756},"天书奇谭.jpg":{"x":1620,"y":0,"width":540,"height":763},"千与千寻.jpg":{"x":2160,"y":0,"width":540,"height":780},"九品芝麻官.jpg":{"x":2700,"y":0,"width":540,"height":867},"龙猫.jpg":{"x":3240,"y":0,"width":540,"height":795},"傲慢与偏见.jpg":{"x":3780,"y":0,"width":540,"height":801},"功夫.jpg":{"x":4320,"y":0,"width":540,"height":754},"指环王2:双塔奇兵.jpg":{"x":4860,"y":0,"width":540,"height":754},"绿皮书.jpg":{"x":5400,"y":0,"width":540,"height":855},"完美的世界.jpg":{"x":5940,"y":0,"width":540,"height":810},"还有明天.jpg":{"x":6480,"y":0,"width":540,"height":768},"泰坦尼克号.jpg":{"x":7020,"y":0,"width":540,"height":803},"花样年华.jpg":{"x":7560,"y":0,"width":540,"height":769},"疯狂动物城.jpg":{"x":8100,"y":0,"width":540,"height":824},"低俗小说.jpg":{"x":8640,"y":0,"width":514,"height":755},"天堂电影院.jpg":{"x":9154,"y":0,"width":540,"height":787},"未麻的部屋.jpg":{"x":9694,"y":0,"width":443,"height":625},"无间道.jpg":{"x":10137,"y":0,"width":540,"height":819},"摩登时代.jpg":{"x":10677,"y":0,"width":540,"height":839},"海上钢琴师.jpg":{"x":11217,"y":0,"width":540,"height":763},"末代皇帝.jpg":{"x":11757,"y":0,"width":540,"height":816},"狮子王.jpg":{"x":12297,"y":0,"width":540,"height":804},"美国往事.jpg":{"x":12837,"y":0,"width":540,"height":799},"触不可及.jpg":{"x":13377,"y":0,"width":540,"height":772},"阳光灿烂的日子.jpg":{"x":13917,"y":0,"width":540,"height":777},"倩女幽魂.jpg":{"x":14457,"y":0,"width":540,"height":756},"教父3.jpg":{"x":14997,"y":0,"width":540,"height":810},"飞越疯人院.jpg":{"x":15537,"y":0,"width":540,"height":802},"死亡诗社.jpg":{"x":16077,"y":0,"width":540,"height":797},"辩护人.jpg":{"x":16617,"y":0,"width":540,"height":773},"爱在黎明破晓前.jpg":{"x":17157,"y":0,"width":540,"height":800},"一个叫欧维的男人决定去死.jpg":{"x":17697,"y":0,"width":540,"height":769},"美丽心灵.jpg":{"x":18237,"y":0,"width":540,"height":801},"神偷奶爸.jpg":{"x":18777,"y":0,"width":540,"height":799},"哪吒闹海.jpg":{"x":19317,"y":0,"width":540,"height":730},"我不是药神.jpg":{"x":19857,"y":0,"width":540,"height":756},"消失的爱人.jpg":{"x":20397,"y":0,"width":540,"height":810},"饮食男女.jpg":{"x":20937,"y":0,"width":404,"height":584},"拯救大兵瑞恩.jpg":{"x":21341,"y":0,"width":540,"height":765},"心灵捕手.jpg":{"x":21881,"y":0,"width":540,"height":796},"美丽人生.jpg":{"x":22421,"y":0,"width":540,"height":756},"时空恋旅人.jpg":{"x":22961,"y":0,"width":540,"height":800},"春光乍泄.jpg":{"x":23501,"y":0,"width":487,"height":711},"让子弹飞.jpg":{"x":23988,"y":0,"width":540,"height":792},"辛德勒的名单.jpg":{"x":24528,"y":0,"width":540,"height":796},"萤火之森.jpg":{"x":25068,"y":0,"width":540,"height":762},"驯龙高手.jpg":{"x":25608,"y":0,"width":540,"height":803},"钢琴家.jpg":{"x":26148,"y":0,"width":540,"height":745},"借东西的小人阿莉埃蒂.jpg":{"x":26688,"y":0,"width":540,"height":762},"哈利·波特与魔法石.jpg":{"x":27228,"y":0,"width":540,"height":799},"大话西游之大圣娶亲.jpg":{"x":27768,"y":0,"width":540,"height":756},"玩具总动员3.jpg":{"x":28308,"y":0,"width":540,"height":799},"乱世佳人.jpg":{"x":28848,"y":0,"width":400,"height":571},"寄生虫.jpg":{"x":29248,"y":0,"width":540,"height":769},"杀人回忆.jpg":{"x":29788,"y":0,"width":540,"height":775},"加勒比海盗.jpg":{"x":30328,"y":0,"width":540,"height":802},"爱在日落黄昏时.jpg":{"x":30868,"y":0,"width":540,"height":797},"怦然心动.jpg":{"x":31408,"y":0,"width":540,"height":802},"阿甘正传.jpg":{"x":31948,"y":0,"width":540,"height":734},"指环王3:王者无敌.jpg":{"x":32488,"y":0,"width":540,"height":754},"蝙蝠侠:黑暗骑士崛起.jpg":{"x":33028,"y":0,"width":540,"height":800},"茶馆.jpg":{"x":33568,"y":0,"width":540,"height":767},"楚门的世界.jpg":{"x":34108,"y":0,"width":540,"height":812},"重庆森林.jpg":{"x":34648,"y":0,"width":540,"height":768},"幸福终点站.jpg":{"x":35188,"y":0,"width":540,"height":770},"菊次郎的夏天.jpg":{"x":35728,"y":0,"width":540,"height":766},"飞屋环游记.jpg":{"x":36268,"y":0,"width":540,"height":800},"控方证人.jpg":{"x":36808,"y":0,"width":450,"height":675},"被嫌弃的松子的一生.jpg":{"x":37258,"y":0,"width":516,"height":728},"机器人总动员.jpg":{"x":37774,"y":0,"width":540,"height":800},"釜山行.jpg":{"x":38314,"y":0,"width":540,"height":772},"看不见的客人.jpg":{"x":38854,"y":0,"width":540,"height":770},"西西里的美丽传说.jpg":{"x":39394,"y":0,"width":540,"height":720},"三傻大闹宝莱坞.jpg":{"x":39934,"y":0,"width":540,"height":777},"寻梦环游记.jpg":{"x":40474,"y":0,"width":540,"height":771},"禁闭岛.jpg":{"x":41014,"y":0,"width":540,"height":800},"教父2.jpg":{"x":41554,"y":0,"width":540,"height":765},"少年派的奇幻漂流.jpg":{"x":42094,"y":0,"width":540,"height":800},"色,戒.jpg":{"x":42634,"y":0,"width":540,"height":770},"哈利·波特与死亡圣器(下).jpg":{"x":43174,"y":0,"width":540,"height":800},"星际穿越.jpg":{"x":43714,"y":0,"width":540,"height":786},"唐伯虎点秋香.jpg":{"x":44254,"y":0,"width":540,"height":757},"喜剧之王.jpg":{"x":44794,"y":0,"width":540,"height":767},"黑客帝国.jpg":{"x":45334,"y":0,"width":540,"height":772},"7号房的礼物.jpg":{"x":45874,"y":0,"width":540,"height":769},"放牛班的春天.jpg":{"x":46414,"y":0,"width":540,"height":756},"哈利·波特与密室.jpg":{"x":46954,"y":0,"width":540,"height":799},"忠犬八公的故事.jpg":{"x":47494,"y":0,"width":540,"height":762},"这个杀手不太冷.jpg":{"x":48034,"y":0,"width":540,"height":765},"活着.jpg":{"x":48574,"y":0,"width":540,"height":797},"致命ID.jpg":{"x":49114,"y":0,"width":540,"height":810},"怪兽电力公司.jpg":{"x":49654,"y":0,"width":540,"height":802},"教父.jpg":{"x":50194,"y":0,"width":540,"height":764},"素媛.jpg":{"x":50734,"y":0,"width":540,"height":769},"新世界.jpg":{"x":51274,"y":0,"width":540,"height":772},"大话西游之月光宝盒.jpg":{"x":51814,"y":0,"width":540,"height":772},"致命魔术.jpg":{"x":52354,"y":0,"width":540,"height":800},"蝙蝠侠:黑暗骑士.jpg":{"x":52894,"y":0,"width":540,"height":800},"本杰明·巴顿奇事.jpg":{"x":53434,"y":0,"width":540,"height":834},"一一.jpg":{"x":53974,"y":0,"width":540,"height":760},"大闹天宫.jpg":{"x":54514,"y":0,"width":540,"height":738},"当幸福来敲门.jpg":{"x":55054,"y":0,"width":540,"height":802},"哈利·波特与阿兹卡班的囚徒.jpg":{"x":55594,"y":0,"width":540,"height":800},"幽灵公主.jpg":{"x":56134,"y":0,"width":540,"height":756},"窃听风暴.jpg":{"x":56674,"y":0,"width":540,"height":810},"猫鼠游戏.jpg":{"x":57214,"y":0,"width":540,"height":799},"甜蜜蜜.jpg":{"x":57754,"y":0,"width":540,"height":756},"闻香识女人.jpg":{"x":58294,"y":0,"width":540,"height":787},"摔跤吧!爸爸.jpg":{"x":58834,"y":0,"width":540,"height":782},"罗马假日.jpg":{"x":59374,"y":0,"width":540,"height":810},"指环王1:护戒使者.jpg":{"x":59914,"y":0,"width":498,"height":778},"入殓师.jpg":{"x":60412,"y":0,"width":540,"height":755},"两杆大烟枪.jpg":{"x":60952,"y":0,"width":540,"height":810},"何以为家.jpg":{"x":61492,"y":0,"width":540,"height":795},"哈利·波特与火焰杯.jpg":{"x":62032,"y":0,"width":540,"height":800},"哈尔的移动城堡.jpg":{"x":62572,"y":0,"width":540,"height":756},"海蒂和爷爷.jpg":{"x":63112,"y":0,"width":540,"height":757},"盗梦空间.jpg":{"x":63652,"y":0,"width":540,"height":799},"超脱.jpg":{"x":64192,"y":0,"width":540,"height":800},"鬼子来了.jpg":{"x":64732,"y":0,"width":540,"height":758},"红辣椒.jpg":{"x":65272,"y":0,"width":540,"height":768},"布达佩斯大饭店.jpg":{"x":65812,"y":0,"width":540,"height":810},"超能陆战队.jpg":{"x":66352,"y":0,"width":540,"height":771},"海豚湾.jpg":{"x":66892,"y":0,"width":540,"height":720},"阿凡达.jpg":{"x":67432,"y":0,"width":400,"height":592},"音乐之声.jpg":{"x":67832,"y":0,"width":540,"height":744},"七宗罪.jpg":{"x":68372,"y":0,"width":540,"height":842},"天使爱美丽.jpg":{"x":68912,"y":0,"width":540,"height":810},"剪刀手爱德华.jpg":{"x":69452,"y":0,"width":498,"height":755},"小森林 夏秋篇.jpg":{"x":69950,"y":0,"width":540,"height":762},"情书.jpg":{"x":70490,"y":0,"width":540,"height":771},"霸王别姬.jpg":{"x":71030,"y":0,"width":540,"height":800},"熔炉.jpg":{"x":71570,"y":0,"width":540,"height":771},"勇敢的心.jpg":{"x":72110,"y":0,"width":540,"height":800},"小鞋子.jpg":{"x":72650,"y":0,"width":407,"height":600}}

const selectedSprite = ref('')

// 计算当前选中图片的样式
const currentSpriteStyle = computed(() => {
  if (!selectedSprite.value || !sprites[selectedSprite.value]) {
    return {}
  }

  const sprite = sprites[selectedSprite.value]

  return {
    width: `${sprite.width}px`,
    height: `${sprite.height}px`,
    backgroundImage: `url(${image.value})`,
    backgroundPosition: `-${sprite.x}px -${sprite.y}px`,
    backgroundRepeat: 'no-repeat',
    backgroundSize: 'auto'
  }
})

const onSpriteChange = () => {
  console.log('选择了图片:', selectedSprite.value)
}

</script>

<template>
  <div class="sprite-container">
    <h2>精灵图显示</h2>

    <select v-model="selectedSprite" @change="onSpriteChange">
      <option value="">请选择图片</option>
      <option v-for="(sprite, name) in sprites" :key="name" :value="name">
        {{ name }}
      </option>
    </select>

    <div v-if="selectedSprite" class="sprite-display">
      <div
          class="sprite-image"
          :style="currentSpriteStyle"
      ></div>
    </div>
  </div>
</template>

<style scoped>
.sprite-container {
  padding: 20px;
  text-align: center;
}

select {
  padding: 10px;
  font-size: 16px;
  border: 1px solid #ddd;
  border-radius: 4px;
  margin: 20px 0;
}

.sprite-display {
  margin: 20px 0;
}

.sprite-image {
  border: 2px solid #007bff;
  border-radius: 8px;
  margin: 10px auto;
  display: inline-block;
}

</style>

效果:


相关推荐
小信丶8 分钟前
Spring Cloud Stream EnableBinding注解详解:定义、应用场景与示例代码
java·spring boot·后端·spring
无限进步_12 分钟前
【C++】验证回文字符串:高效算法详解与优化
java·开发语言·c++·git·算法·github·visual studio
亚历克斯神13 分钟前
Spring Cloud 2026 架构演进
java·spring·微服务
七夜zippoe16 分钟前
Spring Cloud与Dubbo架构哲学对决
java·spring cloud·架构·dubbo·配置中心
海派程序猿17 分钟前
Spring Cloud Config拉取配置过慢导致服务启动延迟的优化技巧
java
阿维的博客日记27 分钟前
为什么不逃逸代表不需要锁,JIT会直接删掉锁
java
William Dawson29 分钟前
CAS的底层实现
java
九英里路40 分钟前
cpp容器——string模拟实现
java·前端·数据结构·c++·算法·容器·字符串
YDS82944 分钟前
大营销平台 —— 抽奖前置规则过滤
java·spring boot·ddd
仍然.1 小时前
多线程---CAS,JUC组件和线程安全的集合类
java·开发语言