
简介本资源是一个基于Java与Spring Boot开发的本地化Mapbox精灵图片Sprite生成与拆分工具面向地图前端开发者、后端Java工程师及需要离线定制地图图标的GIS应用人员。它解决了在无网络或高安全要求环境下无法调用Mapbox在线Sprite服务的痛点支持图标合并、坐标元数据生成、JSON配置输出及按需拆分等核心功能。压缩包共36个文件含31个JAR依赖库涵盖Spring Boot 2.6.3、Logback日志、Jackson JSON处理及图像相关基础组件、2个运行日志文件、1个application.yml配置文件、1个启动脚本start.bat和1个Maven构建配置xml整体大小为14.78MB。目前已有745人学习下载开箱即用附带完整可执行环境与清晰目录结构开发者可直接运行调试、理解Sprite生成逻辑或快速集成至自有地图服务后台。1. 为什么你得自己生成 Mapbox Sprite而不是直接用在线服务Mapbox Sprite 不是 PNG 图片那么简单——它是.json.png的配对资源用于高效渲染图标、标记、符号图层。官方要求 sprite 必须通过 Mapbox Studio 上传并发布但很多企业级项目比如内网 GIS 系统、离线巡检 App、政企保密地图平台根本不能联网上传更麻烦的是一旦图标数量超 500 个或尺寸不规范Studio 会静默失败、报错模糊连400 Bad Request都不告诉你缺哪条字段。这时候“本地生成可运行的 Java 程序”就不是锦上添花而是上线刚需它绕过所有网络依赖把 SVG/ICO/PNG 源文件批量转成 Mapbox 兼容的 sprite atlas并自动拆分大图避免单图超 2048×2048、校验 JSON 结构、生成带像素偏移的坐标映射。本文面向已配好 JDK 11 的 Java 开发者不讲 Mapbox 基础概念只解决“怎么用 Java 把一堆图标变成sprite.json和sprite.png且保证 Mapbox GL JS 或 Android SDK 能直接加载”。2. 为什么选 Java 而不是 Node.js 或 Python核心类库与结构设计Mapbox Sprite 规范本质是两件事图像拼合atlas packing和坐标元数据生成JSON manifest。Node.js 有mapbox/spritezeroPython 有spritesheet但它们要么强依赖 npm/yarn内网无法 install要么 PIL 对透明通道处理不稳定尤其多层叠加的 SVG 导出 PNG 时 alpha 丢失。Java 的优势在于JDK 自带BufferedImage、Graphics2D无外部依赖ImageIO支持 PNG-24 透明通道精准写入且javax.json或轻量org.json可严格控制 JSON 键顺序与数值精度Mapbox GL 对x,y,width,height,pixelRatio的 float 格式敏感1.0和1会被视为不同值。2.1 关键依赖选择零外部 JAR 的最小可行方案我们不引入OpenCV或Apache Commons Imaging——它们体积大、版本冲突风险高。仅用 JDK 本体能力图像拼合BufferedImageGraphics2D手动布局非贪心算法用网格预分配 行优先填充元数据生成org.json:json:20231013轻量、无反射、兼容 JDK 11–21Maven 坐标明确SVG 解析可选若输入含 SVG用Apache Batik 1.17仅当需矢量转栅格时启用否则跳过提示org.json比Jackson或Gson更适合此场景——它不自动序列化double为科学计数法如1e-5而 Mapbox 要求x字段必须是小数点后最多 3 位的十进制数如x: 128.000否则某些旧版 GL Native 会解析失败。2.2 Sprite 生成器的核心类结构可直接抄作业public class MapboxSpriteGenerator { private final int atlasWidth; // 默认 1024最大支持 2048GL 纹理限制 private final int atlasHeight; private final float pixelRatio; // 通常为 1.01x或 2.02x影响 JSON 中 width/height 值 private final ListIconEntry icons new ArrayList(); public static class IconEntry { public final String name; // 图标名将作为 JSON key如 marker-15 public final BufferedImage image; // 已加载的 BufferedImage尺寸 atlas 单元格 public final int originalWidth; // 原图宽用于计算 pixelRatio 缩放 public final int originalHeight; } public MapboxSpriteGenerator(int width, int height, float ratio) { this.atlasWidth width; this.atlasHeight height; this.pixelRatio ratio; } public void addIcon(String name, BufferedImage img) { icons.add(new IconEntry(name, img, img.getWidth(), img.getHeight())); } }这个结构刻意避开复杂布局算法——Mapbox 官方 sprite 推荐使用固定尺寸单元格如 24×24px 1x所以实际做法是先统一缩放所有图标到目标尺寸再按行填满 atlas。这比通用 bin-packing 更稳定、更易调试且完全符合 Mapbox Studio 的输出逻辑。3. 用 Java 在本地跑通 Mapbox Sprite 最小命令从图标目录到可加载资源假设你有一组图标存放在./icons/目录下格式为 PNG含透明通道或 SVG需转 PNG目标生成sprite.json和sprite.png到./dist/。整个流程分三步加载 → 拼合 → 写出。下面代码可直接编译运行JDK 11无需 Maven。3.1 加载图标统一转为 BufferedImage 并校验透明通道private static BufferedImage loadIcon(Path iconPath) throws IOException { String ext Files.getFileExtension(iconPath.toString()).toLowerCase(); BufferedImage img; if (svg.equals(ext)) { // 使用 Batik 将 SVG 渲染为 PNG需添加 batik-all-1.17.jar PNGTranscoder t new PNGTranscoder(); t.addTranscodingHint(PNGTranscoder.KEY_WIDTH, 24f); // 统一输出 24px 宽 t.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, 24f); TranscoderInput input new TranscoderInput(Files.newInputStream(iconPath)); ByteArrayOutputStream os new ByteArrayOutputStream(); TranscoderOutput output new TranscoderOutput(os); t.transcode(input, output); img ImageIO.read(new ByteArrayInputStream(os.toByteArray())); } else { img ImageIO.read(iconPath.toFile()); } // 强制转为 TYPE_INT_ARGB确保 alpha 通道存在 BufferedImage argbImg new BufferedImage(img.getWidth(), img.getHeight(), BufferedImage.TYPE_INT_ARGB); Graphics2D g argbImg.createGraphics(); g.drawImage(img, 0, 0, null); g.dispose(); return argbImg; }注意TYPE_INT_ARGB是关键。如果原图是TYPE_BYTE_INDEXED常见于 8-bit PNGGraphics2D绘制时可能丢弃 alpha。此处强制转换确保后续getRGB()取值准确。3.2 拼合图集网格布局 像素偏移计算private BufferedImage packIcons(ListIconEntry entries, int cols, int cellSize) { int rows (int) Math.ceil((double) entries.size() / cols); BufferedImage atlas new BufferedImage(cols * cellSize, rows * cellSize, BufferedImage.TYPE_INT_ARGB); Graphics2D g atlas.createGraphics(); g.setComposite(AlphaComposite.Src); // 禁用混合直写像素 g.setColor(new Color(0, 0, 0, 0)); // 透明背景 g.fillRect(0, 0, atlas.getWidth(), atlas.getHeight()); for (int i 0; i entries.size(); i) { IconEntry e entries.get(i); int x (i % cols) * cellSize; int y (i / cols) * cellSize; // 居中绘制图标可能小于 cellSize int dx x (cellSize - e.image.getWidth()) / 2; int dy y (cellSize - e.image.getHeight()) / 2; g.drawImage(e.image, dx, dy, null); } g.dispose(); return atlas; }这里cellSize 241x或482x——你必须和pixelRatio保持一致。例如pixelRatio2.0时cellSize48但 JSON 中记录的width仍为24逻辑尺寸x值为48像素坐标。3.3 生成 JSON严格遵循 Mapbox Schemaprivate JSONObject generateSpriteJson(ListIconEntry entries, int cols, int cellSize) { JSONObject sprite new JSONObject(); for (int i 0; i entries.size(); i) { IconEntry e entries.get(i); int x (i % cols) * cellSize (cellSize - e.image.getWidth()) / 2; int y (i / cols) * cellSize (cellSize - e.image.getHeight()) / 2; JSONObject icon new JSONObject(); icon.put(x, x); icon.put(y, y); icon.put(width, e.originalWidth); // 逻辑宽1x 尺寸 icon.put(height, e.originalHeight); // 逻辑高 icon.put(pixelRatio, pixelRatio); // 必须显式写入 sprite.put(e.name, icon); } return sprite; }提示pixelRatio字段不可省略。即使你生成的是 1x 图也必须写pixelRatio: 1.0。Mapbox GL JS 6.0 会据此缩放渲染坐标——缺失该字段会导致图标位置偏移或尺寸错误。3.4 主方法一行命令启动生成public static void main(String[] args) throws Exception { Path iconsDir Paths.get(./icons); Path distDir Paths.get(./dist); // 1. 加载所有 PNG/SVG ListIconEntry icons Files.list(iconsDir) .filter(p - p.toString().toLowerCase().endsWith(.png) || p.toString().toLowerCase().endsWith(.svg)) .map(p - { try { BufferedImage img loadIcon(p); return new IconEntry(Files.getNameWithoutExtension(p), img, img.getWidth(), img.getHeight()); } catch (IOException e) { throw new RuntimeException(Failed to load p, e); } }) .collect(Collectors.toList()); // 2. 初始化生成器2048x2048 1x MapboxSpriteGenerator generator new MapboxSpriteGenerator(2048, 2048, 1.0f); // 3. 添加图标自动缩放至 24x24 icons.forEach(icon - { BufferedImage scaled scaleToSize(icon.image, 24, 24); generator.addIcon(icon.name, scaled); }); // 4. 生成并写出 BufferedImage atlas generator.packIcons(2048 / 24); // 84 列 JSONObject json generator.generateSpriteJson(2048 / 24); ImageIO.write(atlas, PNG, distDir.resolve(sprite.png).toFile()); Files.writeString(distDir.resolve(sprite.json), json.toString(2), StandardCharsets.UTF_8); }运行前确保./icons/下有marker-15.png,building-12.svg等文件文件名即 sprite key./dist/目录存在若含 SVGbatik-all-1.17.jar在 classpath 中执行javac MapboxSpriteGenerator.java java MapboxSpriteGenerator5 秒内生成合规资源。4. 拆分大图与多分辨率支持应对 2048×2048 纹理限制与高清屏适配Mapbox GL 对单张 sprite 图有硬性限制WebGL 纹理最大尺寸为 2048×2048 像素部分低端 GPU 甚至只支持 1024×1024。当图标数超限如 300 个 24×24 图标必须拆分为多个 sprite 文件并在 Mapbox 样式中通过sprite: https://example.com/sprite-{id}引用。Java 程序需支持自动分片。4.1 拆分策略按图标数量切片而非按像素面积常见误区是按atlasWidth × atlasHeight计算剩余空间——但 Mapbox 要求每个 sprite 图必须是正方形1024/2048/4096且 JSON 中每个 icon 的x/y必须在其所属图内坐标系。因此我们采用固定图集尺寸 按序分组图集尺寸单图最大图标数24×24对应 JSON key 前缀1024×102418211024/24 ≈ 42.6 → 42² 1764sprite-0,sprite-12048×204872252048/24 ≈ 85.3 → 85² 7225sprite-0public void generateMultiSprite(ListIconEntry allIcons, int tileSize, String baseName) throws IOException { int iconsPerAtlas (tileSize / 24) * (tileSize / 24); int totalAtlases (int) Math.ceil((double) allIcons.size() / iconsPerAtlas); for (int i 0; i totalAtlases; i) { int start i * iconsPerAtlas; int end Math.min(start iconsPerAtlas, allIcons.size()); ListIconEntry slice allIcons.subList(start, end); BufferedImage atlas packIcons(slice, tileSize / 24, 24); JSONObject json generateSpriteJson(slice, tileSize / 24); String suffix i 0 ? : - i; ImageIO.write(atlas, PNG, Paths.get(./dist, baseName suffix .png).toFile()); Files.writeString(Paths.get(./dist, baseName suffix .json), json.toString(2), StandardCharsets.UTF_8); } }调用方式generator.generateMultiSprite(icons, 2048, sprite)→ 输出sprite-0.png/json,sprite-1.png/json...4.2 多分辨率支持1x、2x、3x 三套图集同步生成高清屏需2x图物理像素翻倍但 JSON 中width/height仍为逻辑尺寸。Java 程序可一次生成三套分辨率图集尺寸cellSizepixelRatio输出文件1x1024×1024241.0sprite1x.png/json2x2048×2048482.0sprite2x.png/json3x3072×3072723.0sprite3x.png/json关键修改在loadIcon后的缩放步骤BufferedImage scaled scaleToSize(icon.image, (int)(24 * ratio), (int)(24 * ratio));然后分别调用generateMultiSprite(..., 1024, sprite1x)等。最终样式中引用{ version: 8, sources: { ... }, sprite: https://domain.com/sprite{ratio} }Mapbox GL 会自动根据设备window.devicePixelRatio选择1x或2x。5. 验证与排错三步确认生成的 sprite 能被 Mapbox 正确加载生成文件不等于可用。必须验证 JSON 结构、PNG 透明度、坐标对齐。以下方法无需 Mapbox 账号纯本地验证。5.1 JSON Schema 校验用官方 JSON Schema 断言字段完整性Mapbox 官方提供 sprite JSON Schema schema.json 。Java 中可用json-schema-validator库dependency groupIdcom.networknt/groupId artifactIdjson-schema-validator/artifactId version1.0.49/version /dependencyString schemaStr Files.readString(Paths.get(schema.json)); JsonSchemaFactory factory JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V7); JsonSchema schema factory.getSchema(schemaStr); JsonNode jsonNode new ObjectMapper().readTree(Files.readString(Paths.get(./dist/sprite.json))); ValidationReport report schema.validate(jsonNode); if (!report.isSuccess()) { report.getMessages().forEach(m - System.err.println(❌ m)); }提示常见失败项包括pixelRatio缺失、x为负数、width为 0。此校验应在 CI 中强制执行。5.2 PNG 透明通道可视化用 Java 读取 Alpha 值直出报告BufferedImage png ImageIO.read(new File(./dist/sprite.png)); int alphaSum 0; for (int y 0; y png.getHeight(); y) { for (int x 0; x png.getWidth(); x) { int rgb png.getRGB(x, y); int alpha (rgb 24) 0xFF; alphaSum alpha; } } double avgAlpha (double) alphaSum / (png.getWidth() * png.getHeight()); System.out.printf(Avg alpha: %.1f (0fully transparent, 255opaque)%n, avgAlpha); // 若 avgAlpha 10 → 整张图几乎透明 → 图标未正确绘制5.3 坐标对齐测试用最小 HTML 页面加载并 inspect 图标创建test.html!DOCTYPE html html head link hrefhttps://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.css relstylesheet / /head body div idmap stylewidth: 400px; height: 300px;/div script srchttps://api.mapbox.com/mapbox-gl-js/v2.15.0/mapbox-gl.js/script script mapboxgl.accessToken your-token; // 任意 token仅用于加载 CDN const map new mapboxgl.Map({ container: map, style: { version: 8, sources: {}, layers: [{ id: test, type: symbol, source: none, layout: { icon-image: marker-15, // 必须与 sprite.json 中 key 一致 icon-size: 1 } }] } }); map.on(load, () { map.addSource(test, { type: geojson, data: { type: FeatureCollection, features: [{ type: Feature, geometry: {type: Point, coordinates: [0, 0]} }] } }); map.addLayer({ id: test-layer, type: symbol, source: test, layout: {icon-image: marker-15} }); }); /script /body /html将./dist/设为 Web 服务器根目录如python3 -m http.server 8000访问http://localhost:8000/test.html。打开浏览器开发者工具 → Network → 查看sprite.json和sprite.png是否 200 OKConsole 是否报Error: Cannot find sprite symbol marker-15。若报此错90% 是 JSON key 名与文件名不一致注意大小写、特殊字符。最后一步用curl -I http://localhost:8000/sprite.json确认响应头含Content-Type: application/json而非text/plain—— Nginx/Apache 需配置 MIME type否则 Mapbox GL 拒绝解析。本文还有配套的精品资源点击获取