├── README.md ├── deps.edn ├── src └── cssfact │ ├── core.clj │ ├── css.clj │ └── matrix.clj └── wrapper └── wrapper.sh /README.md: -------------------------------------------------------------------------------- 1 | # cssfact 2 | 3 | Lossy compression of CSS for fun and loss (or profit) 4 | 5 | ## WTF? 6 | 7 | This program takes CSS and outputs back smaller CSS that hopefully retains some (most) of the information in the input, but contains fewer rules than the original. Exactly how many rules it produces is configurable. 8 | 9 | See [this blog post][0]. 10 | 11 | ## Running it 12 | 13 | You'll need Clojure (follow the [installation instructions][1]). 14 | 15 | You will also need to build the code from [this repo][2]. Follow the instructions in the Makefile, then copy all the resulting `driver*` binaries into one directory. Also, copy the `wrapper.sh` script from this repo to that same directory. 16 | 17 | Edit `src/cssfact/matrix.clj` and point it at the directory with all the binaries and the wrapper script. 18 | 19 | Now, you can run: 20 | 21 | `clojure -M:run -i some-css.css` 22 | 23 | Tip: you can also pass a URL as an input file! 24 | 25 | ## License 26 | 27 | This code is licensed under the [Opinionated Queer License, version 1.1][3]. 28 | 29 | [0]: https://blog.danieljanus.pl/2024/01/26/lossy-css-compression/ 30 | [1]: https://clojure.org/guides/install_clojure 31 | [2]: https://github.com/IBM/binary-matrix-factorization 32 | [3]: https://oql.avris.it/license?c=Daniel%20Janus 33 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | :deps {cheshire/cheshire {:mvn/version "5.12.0"} 3 | net.mikera/vectorz-clj {:mvn/version "0.48.0"} 4 | net.sourceforge.cssparser/cssparser {:mvn/version "0.9.30"} 5 | org.clojure/tools.cli {:mvn/version "1.0.219"}} 6 | :aliases {:run {:main-opts ["-m" "cssfact.core"]}}} 7 | -------------------------------------------------------------------------------- /src/cssfact/core.clj: -------------------------------------------------------------------------------- 1 | (ns cssfact.core 2 | (:require [clojure.tools.cli :as cli] 3 | [cssfact.css :as css] 4 | [cssfact.matrix :as matrix])) 5 | 6 | (defn compress [{:keys [input output num-rules added removed]}] 7 | (println "Reading input...") 8 | (let [input-text (slurp input) 9 | mcss (css/css->matrix (css/parse input-text))] 10 | (printf "%s selectors, %s declarations, %s style rules, %s non-style rules, %s elementary rules.\n" 11 | (count (:selectors mcss)) 12 | (count (:declarations mcss)) 13 | (:original-rules-count mcss) 14 | (count (:other-rules mcss)) 15 | (count (get-in mcss [:matrix :ones]))) 16 | (println "Decomposing matrix...") 17 | (let [decomp (matrix/decompose (:matrix mcss) num-rules) 18 | {:keys [residue num-rules num-added num-removed]} (matrix/residue mcss decomp)] 19 | (printf "%s elementary rules in output, %s removed, %s added\n" num-rules num-removed num-added) 20 | (println "Saving residual files...") 21 | (spit added (css/residual-css mcss residue pos?)) 22 | (spit removed (css/residual-css mcss residue neg?)) 23 | (println "Saving output...") 24 | (let [out-css (css/reconstruct-css mcss decomp)] 25 | (spit output out-css) 26 | (printf "%s saved, %s -> %s characters." output (count input-text) (count out-css)))))) 27 | 28 | (def cli-options 29 | [["-i" "--input CSSFILE" "Input file"] 30 | ["-o" "--output CSSFILE" "Output file" :default "out.css"] 31 | ["-n" "--num-rules N" "Number of rules" :parse-fn #(Long/parseLong %) :validate [pos?] :default 10] 32 | ["-a" "--added CSSFILE" "Residual file of added declarations" :default "added.css"] 33 | ["-r" "--removed CSSFILE" "Residual file of removed declarations" :default "removed.css"]]) 34 | 35 | (defn -main [& args] 36 | (let [{:keys [options summary]} (cli/parse-opts args cli-options)] 37 | (if (:input options) 38 | (compress options) 39 | (printf "Usage: clojure -M -m cssfact.core [OPTIONS]\n\n%s\n" summary)) 40 | (shutdown-agents))) 41 | -------------------------------------------------------------------------------- /src/cssfact/css.clj: -------------------------------------------------------------------------------- 1 | (ns cssfact.css 2 | (:require [clojure.core.matrix :as matrix] 3 | [clojure.java.io :as io] 4 | [clojure.string :as str]) 5 | (:import [java.io StringReader] 6 | [org.w3c.dom.css CSSStyleRule] 7 | [org.w3c.css.sac InputSource] 8 | [com.steadystate.css.dom Property] 9 | [com.steadystate.css.parser CSSOMParser SACParserCSS3])) 10 | 11 | (defn parse [css-string] 12 | (let [parser (CSSOMParser. (SACParserCSS3.)) 13 | input-source (InputSource. (StringReader. css-string))] 14 | (.parseStyleSheet parser input-source nil nil))) 15 | 16 | (defn style-rule->edn [rule] 17 | {:selectors (str/split (.getSelectorText rule) #",\s*") 18 | :declarations (mapv str (.getProperties (.getStyle rule)))}) 19 | 20 | (defn style-rule? [rule] 21 | (instance? CSSStyleRule rule)) 22 | 23 | (defn css->matrix [css] 24 | (let [rules (-> css .getCssRules .getRules) 25 | style-rules (map style-rule->edn (filter style-rule? rules)) 26 | other-rules (remove style-rule? rules) 27 | all-selectors (vec (sort (distinct (mapcat :selectors style-rules)))) 28 | all-declarations (vec (sort (distinct (mapcat :declarations style-rules))))] 29 | {:original-rules-count (count style-rules) 30 | :other-rules other-rules 31 | :selectors all-selectors 32 | :declarations all-declarations 33 | :matrix {:shape [(count all-selectors) (count all-declarations)] 34 | :ones (vec (for [{:keys [selectors declarations]} style-rules 35 | selector selectors 36 | declaration declarations] 37 | [(.indexOf all-selectors selector) 38 | (.indexOf all-declarations declaration)]))}})) 39 | 40 | (defn reconstruct-rule [{:keys [selectors declarations]} [sm bm] i] 41 | (let [sels (matrix/non-zero-indices (matrix/get-column sm i)) 42 | decls (matrix/non-zero-indices (matrix/get-row bm i))] 43 | (when (seq decls) 44 | (str 45 | (str/join ", " (map selectors sels)) 46 | " {\n" 47 | (str/join "\n" (map #(str " " (declarations %) ";") decls)) 48 | "\n}\n")))) 49 | 50 | (defn reconstruct-css [mcss decomp] 51 | (let [[_ n] (matrix/shape (first decomp))] 52 | (apply str 53 | (str/join "\n" (:other-rules mcss)) 54 | "\n" 55 | (map (partial reconstruct-rule mcss decomp) (range n))))) 56 | 57 | (defn residual-css [{:keys [selectors declarations]} residue filter-fn] 58 | (apply str 59 | (for [[x slice] (map-indexed vector (matrix/non-zero-indices residue)) y slice 60 | :when (filter-fn (matrix/mget residue x y))] 61 | (format "%s { %s; }\n" (selectors x) (declarations y))))) 62 | -------------------------------------------------------------------------------- /src/cssfact/matrix.clj: -------------------------------------------------------------------------------- 1 | (ns cssfact.matrix 2 | (:require [cheshire.core :as json] 3 | [clojure.core.matrix :as matrix] 4 | [clojure.core.matrix.operators :as matrix.ops] 5 | [clojure.java.io :as io] 6 | [clojure.java.shell :as sh])) 7 | 8 | ;; Edit this to point at the directory where you've downloaded 9 | ;; and compiled binary-matrix-factorization 10 | (def bmf-dir "/Users/nathell/projects/vendor/binary-matrix-factorization/bin") 11 | 12 | (matrix/set-current-implementation :vectorz) 13 | 14 | (defn bmmul [x y] 15 | (matrix/emap #(if (pos? %) 1 0) (matrix/mmul x y))) 16 | 17 | (defn ->dense [{:keys [shape ones]}] 18 | (let [[n m] shape] 19 | (matrix/set-indices! (matrix/zero-matrix n m) 20 | ones 21 | (repeat (count ones) 1)))) 22 | 23 | (defn parse-txt [filename] 24 | (with-open [rdr (io/reader filename)] 25 | (let [lines (line-seq rdr)] 26 | (matrix/matrix (mapv #(read-string (str "[" % "]")) lines))))) 27 | 28 | (defn save-csv [outfile {:keys [shape ones]}] 29 | (with-open [w (io/writer outfile)] 30 | (binding [*out* w] 31 | (println "n,m") 32 | (let [[n m] shape] 33 | (printf "%s,%s\n" n m) 34 | (println "i,j,x_ij") 35 | (doseq [[i j] ones] 36 | (printf "%s,%s,%s\n" i j 1)))))) 37 | 38 | (defn decompose 39 | ([matrix rank] (decompose matrix rank 1)) 40 | ([matrix rank init-alg] 41 | (let [temp-file (java.io.File/createTempFile "matrix" ".csv")] 42 | (try 43 | (save-csv temp-file matrix) 44 | (sh/sh (str bmf-dir "/wrapper.sh") (str temp-file) (str rank) (str init-alg)) 45 | [(parse-txt (str bmf-dir "/Sout.txt")) 46 | (parse-txt (str bmf-dir "/Bout.txt"))] 47 | (finally 48 | (.delete temp-file)))))) 49 | 50 | (defn residue [{:keys [matrix]} [sm bm]] 51 | (let [m (->dense matrix) 52 | m' (bmmul sm bm) 53 | residue (matrix/emap - m' m)] 54 | {:residue residue 55 | :num-rules (matrix/non-zero-count m') 56 | :num-added (count (filter pos? (matrix/eseq residue))) 57 | :num-removed (count (filter neg? (matrix/eseq residue)))})) 58 | -------------------------------------------------------------------------------- /wrapper/wrapper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | cd "$(dirname $0)" 3 | echo $* 4 | exec ./driver $* 5 | --------------------------------------------------------------------------------