/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jooby.assets; import static com.eclipsesource.v8.utils.V8ObjectUtils.toV8Array; import static com.eclipsesource.v8.utils.V8ObjectUtils.toV8Object; import java.io.File; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.FileReader; import java.io.IOException; import java.io.OutputStream; import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.batik.transcoder.TranscoderInput; import org.apache.batik.transcoder.TranscoderOutput; import org.apache.batik.transcoder.image.PNGTranscoder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.eclipsesource.v8.V8; import com.eclipsesource.v8.V8Function; import com.google.common.hash.Hasher; import com.google.common.hash.Hashing; import com.google.common.io.BaseEncoding; import com.typesafe.config.Config; import javaslang.control.Try; /** * <h1>svg-sprites</h1> * <p> * An {@link AssetAggregator} that creates SVG sprites with PNG fallbacks at needed sizes via * <a href="https://github.com/drdk/dr-svg-sprites">dr-svg-sprites</a>. * </p> * * <h2>usage</h2> * * <pre> * assets { * fileset { * sprite: svg-sprites * home: home.scss * } * * svg-sprites { * spriteElementPath: "images/svg-source", * spritePath: "css" * } * } * </pre> * * <p> * The <code>spriteElementPath</code> contains all the <code>*.svg</code> files you want to process. * The <code>spritePath</code> indicates where to save the sprite, here you will find the following * generated files: <code>css/sprite.css</code>, <code>css/sprite.svg</code> and * <code>css/sprite.png</code>. * </p> * * <h2>options</h2> * * <pre> * assets { * fileset { * sprite: svg-sprites * home: home.scss * } * * svg-sprites { * spriteElementPath: "images/svg-source", * spritePath: "css", * layout: "vertical", * sizes: { * large: 24, * small: 16 * }, * refSize: "large" * } * } * </pre> * * <p> * Please refer to <a href="https://github.com/drdk/dr-svg-sprites">dr-svg-sprites</a> for more * details. * </p> * * @author edgar * */ public class SvgSprites extends AssetAggregator { /** The logging system. */ private final Logger log = LoggerFactory.getLogger(getClass()); @Override public SvgSprites set(final Config options) { super.set(options); return this; } @Override public SvgSprites set(final String name, final Object value) { super.set(name, value); return this; } @Override public List<String> fileset() { return Arrays.asList(cssPath()); } @Override public void run(final Config conf) throws Exception { File spriteElementPath = resolve(get("spriteElementPath").toString()); if (!spriteElementPath.exists()) { throw new FileNotFoundException(spriteElementPath.toString()); } File workdir = new File(Try.of(() -> conf.getString("application.tmpdir")) .getOrElse(System.getProperty("java.io.tmpdir"))); File spritePath = resolve(spritePath()); File cssPath = resolve(cssPath()); String sha1 = new File(spritePath()).getName() .replace(".svg", "-" + sha1(spriteElementPath, spritePath, cssPath) + ".sha1"); File uptodate = workdir.toPath().resolve("svg-sprites").resolve(sha1).toFile(); if (uptodate.exists()) { log.info("svg-sprites is up-to-date: {}", uptodate); return; } Nodejs.run(workdir, node -> { node.overwrite(conf.hasPath("_overwrite") ? conf.getBoolean("_overwrite") : false) .exec("dr-svg-sprites", v8 -> { Map<String, Object> options = options(); // rewrite paths options.put("spritePath", spritePath.toString()); options.put("cssPath", cssPath.toString()); options.put("spriteElementPath", spriteElementPath.toString()); log.debug("svg-sprites options {} ", options.entrySet().stream() .map(e -> e.getKey() + ": " + e.getValue()) .collect(Collectors.joining("\n ", "{\n ", "\n}"))); v8.add("$options", toV8Object(v8, options)); /** * Hook sv2png and remove panthomjs dependency. */ v8.add("svg2png", new V8Function(v8, (receiver, params) -> { String svgPath = params.get(0).toString(); String pngPath = params.get(1).toString(); Float w = new Float(params.getDouble(2)); Float h = new Float(params.getDouble(3)); V8Function callback = (V8Function) params.get(4); Try.run(() -> { try (FileReader in = new FileReader(svgPath); OutputStream out = new FileOutputStream(pngPath)) { PNGTranscoder transcoder = new PNGTranscoder(); transcoder.addTranscodingHint(PNGTranscoder.KEY_WIDTH, w); transcoder.addTranscodingHint(PNGTranscoder.KEY_HEIGHT, h); transcoder.transcode(new TranscoderInput(in), new TranscoderOutput(out)); } }) .onSuccess(v -> callback.call(null, null)) .onFailure(x -> { log.debug("png-fallback resulted in exception", x); callback.call(null, toV8Array(v8, Arrays.asList(x.getMessage()))); }); return V8.UNDEFINED; })); }); }); log.debug("creating sha1: {}", uptodate); uptodate.getParentFile().mkdirs(); // clean old/previous *.sha1 files try (Stream<Path> sha1files = Files.walk(uptodate.getParentFile().toPath()) .filter(it -> it.toString().endsWith(".sha1"))) { sha1files.forEach(it -> Try.run(() -> Files.delete(it))); } Files.createFile(uptodate.toPath()); uptodate.deleteOnExit(); } private String sha1(final File dir, final File sprite, final File css) throws IOException { try (Stream<Path> stream = Files.walk(dir.toPath())) { Hasher sha1 = Hashing.sha1().newHasher(); stream.filter(p -> !Files.isDirectory(p)) .forEach(p -> Try.run(() -> sha1.putBytes(Files.readAllBytes(p)))); if (sprite.exists()) { sha1.putBytes(Files.readAllBytes(sprite.toPath())); } if (css.exists()) { sha1.putBytes(Files.readAllBytes(css.toPath())); } return BaseEncoding.base16().encode(sha1.hash().asBytes()).toLowerCase(); } } private File resolve(final String path) { // basedir is set to public from super class return new File(get("basedir").toString(), path); } public String cssPath() { try { return nameFor("cssPath", ".css"); } catch (IllegalArgumentException x) { return spritePath().replace(".svg", ".css"); } } public String spritePath() { return nameFor("spritePath", ".svg"); } private String nameFor(final String property, final String ext) { String spritePath = get(property); if (spritePath == null) { throw new IllegalArgumentException( "Required option 'svg-sprites." + property + "' not present"); } if (spritePath.endsWith(ext)) { return spritePath; } else { return spritePath + "/" + prefix("prefix") + prefix("name") + "sprite" + ext; } } private String prefix(final String name) { return Optional.ofNullable(get(name)) .map(it -> it + "-") .orElse(""); } }