bread-theme: native palette extraction, replacing pywal #36
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "feature/native-palette"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Removes the
wal/ImageMagick subprocess dependency from bread-theme (epic PYWAL).What changed
bread-theme/tests/fixtures/, captured bybread-theme/scripts/capture-goldens.py: color1-6 from a realwal -irun in an isolatedHOME, pluswal_colors, the full ordered list pywal's own backend (pywal.backends.wal.gen_colors) builds. Fixtures: the BOS default wallpaper (PNG, baseline JPEG, progressive JPEG with restart markers, lossy WebP, 24-bit BMP, palette BMP, two-frame GIF), dark/moody (RGBA), saturated/colourful (4-bit palette), bright/screenshot-like (RGBA), near-monochrome (8-bit gray) and near-monochrome-16bit (16-bit gray, see below).palette_from_imageis a native Rust extractor that reproduceswal -iexactly, keeping its public signature.bread-theme/src/extract.rsports the ImageMagick 7.1.2 (Q16 HDRI) code behind pywal'smagick "<img>[0]" -resize 25% -colors N -unique-colors txt:-:ParseMetaGeometryrounding, default filter selection (Lanczos, or Mitchell for images with alpha or PseudoClass images such as palette PNGs, 8-bit gray PNGs, gray JPEGs, GIFs and palette BMPs), two separable passes in ImageMagick's order, alpha-weighted colour channels, results kept as unclampedfloatquantums;IdentifyImageGray/SetGrayscaleImage), tree depth from N reduced for the default dither,ClassifyImageColors(full-depth phase until the colour count exceeds N, thenPruneToCubeDepth),ReduceImageColorsthresholds (QuantizeErrorFlatten,Reduce,PruneChild),DefineImageColormapmeans cast to float quantums, and the Riemersma Hilbert-curve dither with its 16-entry error queue and 6-bit closest-colour cache;-unique-colors:histogram.ctree order (child idr | g<<1 | b<<2, so blue most significant and red least; first appearance within a leaf);txt:+ pywal's#.{6}regex, including its truncation of 16-bit output.JPEGs are decoded by
bread-theme/src/jpeg.rs, a port of libjpeg-turbo 3.2's default decode path (islow IDCT, fancy upsampling, YCbCr tables, context-row edges, colour space guess), because zune-jpeg's samples differ from libjpeg-turbo's by up to 5 levels, which was enough to flip slots. It covers baseline and progressive Huffman JPEGs; arithmetic-coded, 12-bit, lossless, CMYK and block-smoothed progressive files fall back to theimagecrate. Theimagecrate now also decodes WebP, GIF (first frame, at its own size, alpha only if it declares a transparent index) and BMP. A decode failure is still anErr. New publicpywal_colors()returns the full list.palette_cache_path()is$XDG_CACHE_HOME/bread/palette.json(walcolors.jsonshape), written atomically bywrite_global_palette.load_palette()reads that path first, falls back to~/.cache/wal/colors.jsonfor upgrading installs, thenPalette::default().palette_cache_dir()is also exported as a stable directory for a long-lived watcher (breadman) to watch.write_global_palettealso writes a terminal colour-sequences file (sequences_path(), OSC 4 for all 16 ANSI slots plus OSC 10/11/12 for fg/bg/cursor), so BOS's skel.zshrc(which cats this file) keeps working with nowalin the picture. Terminals now also get the fixed BOS dark background instead of a raw wallpaper-derived one.python-pywalfrombakery.toml's optional deps, rewords Cargo.toml/registry descriptions and CLI help, and updates README / BREAD_DESIGN_SYSTEM.md / docs/shell-themes.md / CHANGELOG to describe native extraction instead of pywal.Fidelity against real pywal
Measured with
bread-theme/scripts/compare-pywal.py(runs pywal's exactmagickloop and regex againstexamples/pywal-colors.rs; ImageMagick 7.1.2-31 Q16-HDRI, libjpeg-turbo 3.2.0):~/Pictures/Backgrounds(21 decodable real wallpapers: 13 JPEG, 7 PNG incl. RGBA and palette, 1 WebP; up to 8192x5956) + BOSBread Background.pngThe one file in the corpus that fails,
1402516.gif, is a 0-byte file; wal fails on it too. The JPEG decoder was also checked byte-for-byte against ImageMagick's decode on all 13 corpus JPEGs and on 4:2:0, 4:2:2, 4:4:0, 4:1:1, progressive, restart-marker, grayscale and tiny (3x2, 7x5) JPEGs: 0 differing bytes.16-bit quirk (ECO-29 review): ImageMagick's
txt:prints 16-bit images as#RRRRGGGGBBBB, and pywal's#.{6}keeps#RRRRGG(red high byte, red low byte, green high byte). That is where the green/purple accents in the old near-monochrome golden came from, not dithering. The extractor reproduces it on purpose, since matching what users got from pywal is the requirement. The fixture is kept asnear-monochrome-16bit.pngto pin the quirk, andnear-monochrome.pngis now the same gradient at 8 bits (grey accents).Remaining deviations: none measured. Known untested corners where the port falls back or could differ: JPEG features libjpeg-turbo supports but
jpeg.rshands to theimagecrate (arithmetic coding, 12-bit, lossless, CMYK/YCCK, progressive files with never-sent low-frequency coefficients, which libjpeg-turbo block-smooths), animated WebP, BMP variants with bitfield alpha, PNGgAMA=1.0 files (ImageMagick may tag them linear RGB), and the order of near-equal (within 1e-12) quantize errors, where ImageMagick's comparator and glibcqsortcould pick a different element. Output is tied to ImageMagick 7.1.2's algorithms; a future ImageMagick that changes quantize.c would change pywal's own output too.Timing (release, 16 threads for resize and IDCT, best of 3):
wal -i(uncached)-colorsretries)Licensing
extract.rsports ImageMagick logic (ImageMagick License) andjpeg.rsports libjpeg-turbo code (IJG License and Modified BSD). Addedbread-theme/NOTICE(what was ported, copyright lines),bread-theme/LICENSE-ImageMagickandbread-theme/LICENSE-libjpeg-turbo(verbatim licence texts). Because bakery ships only the bare binary,bread-theme noticeprints NOTICE plus both licence texts from inside the binary (IJG's "based in part on the work of the Independent JPEG Group" statement is in it). bread-theme's crate licence is nowMIT AND ImageMagick AND IJG AND BSD-3-Clause; the rest of the workspace stays MIT.Verification
cargo fmt --all -- --checkcargo clippy --workspace --all-targets --locked -- -D warningsand with--features gtkcargo test --workspace --lockedand with--features gtk(151 bread-theme tests, including exact per-slot and full-list golden tests for every fixture, determinism, 16-bit truncation, GIF first frame, WebP/BMP/JPEG decoding, JPEG bit-exactness against libjpeg-turbo references, no near-black accents on real photos, decode errors stayingErr)cargo build --workspace --locked, Cargo.lock updated for the webp/gif/bmp decoderscompare-pywal.pyruns and timing as aboveNotes for reviewers
write_wal_cache/wal_cache_json/wal_cache_pathare kept as-is for third-party pywal-template users; nothing in bread depends on them anymore.palette_cache_dir()literal~/.cachefallback nit from the last review is untouched (ECO-31 was approved as is).Closes ECO-29
Closes ECO-30
Closes ECO-31
Closes ECO-32
Closes ECO-33
wal -i#30Review: changes requested (not merging)
CI is green and the PR is mergeable. The cache, sequences and docs work (ECO-31/32/33) is fine. The extractor (ECO-30) and one golden fixture (ECO-29) do not meet the requirement. The user asked for output "as close as possible to pywal's output for the same image", and this is not close.
Measured fidelity (reviewer re-run, ImageMagick 7.1.2-31 Q16-HDRI)
The harness runs pywal's exact pipeline (
magick "<img>[0]" -resize 25% -colors N -unique-colors txt:-, N from 16 to 35,#.{6}regex,colors[8:14]) againstbread-theme generate-output --imagebuilt fromcf61b0b. Its wal side reproduces the committed goldens byte for byte, so the harness is sound.Bread Background.pngAlso found:
#000002 #000003 #000102 ...), and the same six values come back for several unrelated images (Beast Gohan SBC.png, Megumi Purple.jpeg, 1341491.png).Spirit Bomb.webpworks with wal but errors here.1402516.giferrors too.Change requests
bread-theme/src/extract.rs:175-199(quantize_like_wal) has a degenerate fallthrough. Photos often have no octree level whose bucket count is between 16 and the target. Depth 1 has 8 buckets or fewer, and depth 2 is often over 35. When that happens the loop never returns, and the function returnsby_depth[8], which is every distinct colour.accents_from_colorsthen takes indices 8..13 of that Z-ordered list: the darkest near-black raw colours. The comment at 193-198 assumes that list is short. It is not. This is the cause of the near-black accents above. It goes away if the quantizer is replaced (next item). Whatever lands must have a regression test on a real photo that asserts the accents are not near-black.MagickCore/quantize.c:QuantizeImage: how the tree depth is derived from the colour count, and the depth reduction when dithering is on.ClassifyImageColors: per-node sums andquantize_error, and the pruning when the node count exceeds its cap.ReduceImageColors/Reduce/PruneChild: the pruning threshold that grows until the leaf count fits.AssignImageColors/DefineImageColormap: colormap entry = node mean, with IM's rounding. Also the default Riemersma dither, which decides which colormap entries end up used by at least one pixel. That is what-unique-colorslists.MagickCore/histogram.cUniqueImageColors/ColorToNodeId: the traversal order. The currentbucket_key(extract.rs:127-138) puts red in the most significant bit of each level. The real wal output is ordered with blue as the most significant bit and red as the least (ColorToNodeIdbuildsidas red bit | green bit << 1 | blue bit << 2). Evidence from Beast Gohan SBC.png: wal's list#562C61 #4A4261 #A33962 #325696 #5C609B #4E46AB #B34DBA #59AEDA #9DA2E7 ...has top-bit node ids 0,0,1,4,4,4,5,6,7, which is monotonic only with R as the low bit. Even a perfect quantizer picks the wrong slots for color1..6 with the current ordering.MagickCore/resize.c: the default filter IM picks for a 25% downscale, and how-resize 25%rounds the target geometry. Confirm Lanczos3 on real 1920x1080 inputs, not only the 96x96 fixture.ScaleQuantumToChar).Cargo.toml:25). Enable at leastwebp,gif,bmpon theimagecrate, since breadpaper accepts all of them (breadpaper/src/lib.rs:24).image::openon a GIF decodes the first frame, which matches wal's[0]. Add a webp and a gif fixture test. Decode failure already returnsErr(extract.rs:62-66), which is the right contract for anio::Result. Keep it that way; do not make it fall back to defaults.tests/fixtures/golden/near-monochrome.jsonis corrupt, and the test comment explaining it is wrong. The fixture is a 16-bit grayscale PNG. For 16-bit input IM'stxt:output prints 12-digit hex (#78F078F078F0), and pywal's#.{6}regex truncates that to#78F078. That is where the "green/purple" comes from. It is not a dithering artifact (output.rs:658-668). Either regenerate this fixture as 8-bit, or keep it and document it as a pywal parsing bug that bread-theme deliberately does not reproduce. Fix the explanation either way.assert_hue_family(output.rs:600-617) passes if any one of six accents is within 45 degrees of the golden mean hue. An extractor returning six unrelated colours can pass it. Replace it with per-slot assertions against the goldens.bread-theme/tests/orscripts/), so anyone withmagickinstalled can re-run it. Numbers go in the PR body.output.rs:164-168. Thepalette_from_imagedoc still says "median cut ... six accent hues spread across the hue wheel". Update it to describe what the code does.Nits (non-blocking)
palette_cache_dir()falls back toPathBuf::from("~/.cache")(output.rs:181-185). A literal tilde is never expanded. It copies the existingwal_pathpattern, but consider$HOME/.cacheor an error.Acceptance criteria as I read them
near-monochrome(corrupt, see above).palette_from_imageis native, keeps its signature, and matches wal as closely as possible. Not met (fidelity and degenerate fallthrough).palette.jsoncache, atomic write,load_palettereads it first and falls back to~/.cache/wal. Met:output.rs:193,238-246,palette.rs:81-92, with tests.sequences_path(). Met:output.rs:202-236, tested.Rework happens on the existing branch
feature/native-palette, and this PR stays open. Do not open a second PR for these keys. No release or tag until this is resolved. Consumers (breadpaper#6, breadpad#9, bos-settings#18) should re-point to the release tag only after that.wal -i#30Goldens are now captured by scripts/capture-goldens.py: color1..color6 from real `wal -i` in an isolated HOME, plus "wal_colors", the full ordered list pywal's backend built (pywal.backends.wal.gen_colors), so tests can assert every slot rather than a hue family. near-monochrome.png was a 16-bit grayscale PNG. ImageMagick's txt: output prints 16-bit images as #RRRRGGGGBBBB and pywal's `#.{6}` regex keeps "#RRRRGG", which is where its green/purple accents came from (not dithering). It is kept as near-monochrome-16bit.png to pin that quirk, and near-monochrome.png is now the same gradient at 8 bits (grey accents). New fixtures derived from the BOS default wallpaper cover baseline and progressive (with restart markers) JPEG, lossy WebP, 24-bit and palette BMP, and a two-frame GIF; tests/fixtures/jpeg holds two small odd-sized JPEGs with ImageMagick's own decode as PPM references.The native extractor now reproduces `wal -i` output exactly instead of approximating it. extract.rs is a port of the ImageMagick 7.1.2 (Q16 HDRI) code behind pywal's `magick "<img>[0]" -resize 25% -colors N -unique-colors txt:-`: - resize: ParseMetaGeometry rounding, default filter selection (Lanczos, or Mitchell for alpha/PseudoClass images), two separable passes in ImageMagick's order with alpha-weighted colour channels and unclamped float quantums. - quantize: gray detection, tree depth from N (reduced for the default dither), ClassifyImageColors with its full-depth phase and PruneToCubeDepth, ReduceImageColors thresholds, DefineImageColormap means cast to float quantums, and the Riemersma Hilbert-curve dither with its error queue and 6-bit closest-colour cache. - unique colours: histogram.c tree order (blue most significant, red least; first appearance within a leaf). - output: txt: hex formatting, including pywal's `#.{6}` truncation of 16-bit output. JPEGs are decoded by a new jpeg.rs, a port of libjpeg-turbo's default path (islow IDCT, fancy upsampling, YCbCr tables), because zune-jpeg's samples differ by a few levels and that was enough to flip slots. The image crate now also decodes WebP, GIF (first frame, at its own size) and BMP. Tests assert exact per-slot and full-list equality with the goldens, plus determinism, 16-bit truncation, GIF first frame, decoder bit-exactness, no near-black accents on real photos, and decode errors staying Err. scripts/compare-pywal.py and examples/pywal-colors.rs compare against a real magick at dev time. pywal_colors() exposes the full list. The ported code is under the ImageMagick License and the IJG/BSD-3 licences: NOTICE, LICENSE-ImageMagick and LICENSE-libjpeg-turbo are added, `bread-theme notice` prints them for binary-only installs, and the crate licence is now "MIT AND ImageMagick AND IJG AND BSD-3-Clause".Rework for the ECO-29/ECO-30 change requests (#issuecomment-1075)
Two new commits on
feature/native-palette. The ECO-31/32/33 work is unchanged.6e34f3btest(bread-theme): exact pywal goldens with full colour lists and format fixtures3758f0cfeat(bread-theme): port pywal's ImageMagick pipeline for exact palette parityChecklist
quantize_like_wal): removed along with the octree approximation.real_photo_accents_are_never_near_blackguards it on real photo fixtures, and the corpus run below has 0 near-black accent sets (was 7 of 20).extract.rsports ImageMagick 7.1.2 Q16-HDRI from source: ParseMetaGeometry rounding; ResizeImage filter choice (Lanczos, or Mitchell for alpha or PseudoClass input) and two-pass order with alpha blending and unclamped float quantums; QuantizeImage depth (4, minus 1 for dither = 3, or 8 for gray); ClassifyImageColors (both phases, run-lengthIsPixelEquivalent,quantize_erroraccumulation); ReduceImageColors/Reduce/PruneChild; DefineImageColormap; Riemersma dither including the 6-bitCacheOffsetcolour cache (this is what decides which entries survive into-unique-colors); histogram.c ordering (confirmed:r | g<<1 | b<<2, so blue is most significant); ScaleQuantumToChar float rounding.jpeg.rsports libjpeg-turbo 3.2's default decode and is byte-identical to ImageMagick's decode on all 13 corpus JPEGs and on synthetic 4:2:0/4:2:2/4:4:0/4:1:1/progressive/restart/gray/tiny files.webp,gifandbmpenabled. For GIF, the first frame is used at its own size, with alpha only if it declares a transparent index (ImageMagick's[0]). Decode failure stays anErr, and a test covers garbage, truncated and missing files.#.{6}truncating 16-bit#RRRRGGGGBBBBoutput. Since the requirement is pywal's output, the extractor reproduces the quirk. The fixture is kept asnear-monochrome-16bit.png(tested),near-monochrome.pngis now 8-bit, and the wrong dithering comment is gone.assert_hue_familyis replaced by exact per-slot equality on color1-6 and on the fullwal_colorslist for 12 golden fixtures, plus tests for determinism, 16-bit truncation, GIF frame 0 (with a check that frame 1 would differ), JPEG bit-exactness against IM-decoded PPM references, histogram order, geometry rounding and filter kernels.bread-theme/scripts/compare-pywal.py [--full] IMAGE_OR_DIR..., which needsmagickat dev time only.scripts/capture-goldens.pyregenerates goldens from realwal.palette_from_imagedoc, the module docs and the CHANGELOG are rewritten.NOTICE,LICENSE-ImageMagick,LICENSE-libjpeg-turbo, andbread-theme notice(the binary ships without the source tree). The crate licence isMIT AND ImageMagick AND IJG AND BSD-3-Clause.Measurements (compare-pywal.py, IM 7.1.2-31 Q16-HDRI)
Bread Background.png4K timing (release, best of 3), native vs uncached
wal -i: 3840x2160 PNG 0.09 s vs 0.29 s, 3840x2160 RGBA PNG 0.10 s vs 0.36 s, 3840x2160 JPEG 0.16 s vs 0.30 s, 8192x5956 progressive JPEG (8 retries) 2.5 s vs 9.9 s.Remaining deviations: none measured. The PR body lists the untested corners (JPEG variants that fall back to zune, and exotic BMP/PNG variants).
Verification: fmt check, clippy
-D warningswith and without--features gtk,cargo test --workspace --lockedwith and without gtk (151 bread-theme tests), andcargo build --workspace --locked, all green locally.wal -i#30wal -i#30Second-round review: one robustness fix requested (release-sensitive repo, not merging)
CI is green on
3758f0c(check / check, run 214) and the PR is mergeable.cargo fmt --check,clippy -p bread-theme --all-targets -D warningsandcargo test -p bread-theme(139 passed) are clean locally.Parity: independently confirmed
I did not use
compare-pywal.py. My own harness runs realwal -i <img> -n -q -s -t -e(pywal 3.3.0, ImageMagick 7.1.2-31 Q16-HDRI) withHOME/XDG_CACHE_HOMEpointed at a fresh temp dir per image, readscolors.jsoncolor1..6, and compares them tobread-theme generate-output --image(release build, isolatedXDG_RUNTIME_DIR). It also compares the full list fromexamples/pywal-colorswith the rawmagick ... txt:-list.The fresh corpus is real photos and screenshots from ~/Downloads and ~/Pictures (phone JPEGs, posters, a 1.7 MB GIF, a removebg RGBA PNG) plus generated variants: odd 1001x667, 4K and 8K JPEG, progressive, 4:4:4/4:2:2 with restart interval/4:1:1/4:4:0 subsampling, quality 5, 50% alpha, gradient alpha on a palette PNG, palette PNG, gray PNG/JPEG, 16-bit PNG, lossy and lossless WebP, GIF, 24-bit and 8-bit BMP, plasma, random noise. The one mismatch is a CMYK JPEG: 4/6 accents exact, and the other two are off by one level in one or two channels (
#89774bvs#88774b,#669fb3vs#66a0b4). That is the documentedimagecrate fallback. Where wal gives up (1x1, 7x5, 3x2, solid, black, gradient, checkerboard, fully transparent), bread-theme returns the curated defaults, as designed.Timing: 4K JPEG 0.21 s vs 0.44 s for wal, 8K JPEG 0.50 s vs 1.06 s. Peak RSS on the 8K JPEG is 337 MB, vs 516 MB for
magickalone.The round-1 findings are resolved: parity, near-black accents, unique-colour ordering, exact tests, webp/gif/bmp.
Robustness
A fuzz pass mutated 9 sources (baseline, progressive and restart JPEG, CMYK JPEG, palette PNG, 16-bit PNG, WebP, GIF, 8-bit BMP) 60 times each: truncation, random byte flips and header byte flips. That is 540 cases with no panic and no hang. Zero-width and zero-height JPEG/PNG return
Err, and oversized PNG IHDRs returnErr("Memory limit exceeded").Change requests
bread-theme/src/jpeg.rs:355-365(and:439,:480). The SOF handler sizesc.coefs = vec![[0i16; 64]; c.bw * c.bh]from the declared width and height, before any scan data is read, with no cap. To test it I took a 22 KB baseline JPEG and changed only its SOF dimensions to 65500x65500.examples/pywal-colorson that file reached 11.4 GB RSS within 1.6 s and was then killed by SIGTERM (oomd) on this 32 GB machine.extract.rs:204tries this decoder beforeimage::load_from_memory(extract.rs:217), so it bypasses theimagecrate's default 512 MiB allocation limit, which is what protects the PNG/WebP/GIF/BMP paths. breadpaper calls this in-process on a user-chosen file. Fix: in the SOF branch, check the total coefficient and output buffer size against a documented budget before allocating. Use checked multiplication and a named constant sized for real wallpapers, for example 8K and 12000x8000 photos. ReturnNone(or an error) when the budget is exceeded. Add a test that builds a tiny JPEG with a huge SOF and assertsErrwithout allocating.imagepath. Right now a legitimately huge 16-bit RGBA PNG (about 12000x8000) fails with "Memory limit exceeded", where wal succeeds. Set explicitimage::Limitsso both decoders share one documented ceiling. A doc comment stating the limit is also fine.Okwith 1/6 accents matching wal. Wal also succeeds on it, because libjpeg pads the missing data. The data is neither exact nor anErr. Either reproduce libjpeg's premature-EOF fill, or returnErrso breadpaper's extract-before-apply refuses the file. Pick one and test it (the current truncation test only covers a 40-byte file).src/adw.rs:21,src/gtk.rs:121,351,src/layerrules.rs:204,src/shell/mod.rs:1178,1328,1465,src/shell/style.rs:37,src/shell/types.rs:796, and the fourassets/shell/*/theme.tomlcomments. This is ECO-33 scope creep, so it is optional, but it is cheap to fix while the branch is open.Licensing: needs the user's decision, not a code change
MIT AND ImageMagick AND IJG AND BSD-3-Clauseuses valid identifiers. However, libjpeg-turbo's LICENSE.md applies BSD-3-Clause only to the TurboJPEG API, libspng and the build system. The files ported here (jidctint.c, jdsample.c, jdcolor.c, jdhuff.c/jdphuff.c, jdmaster.c, jdmainct.c, jdapimin.c) are libjpeg API code, which is IJG-only. SoBSD-3-Clauseis over-inclusive. Keeping it is harmless and conservative. Dropping it is more accurate.bread-theme/NOTICEhas the copyright line but not ImageMagick's upstream NOTICE text. Check whether that text should be copied in verbatim.palette_from_imagenow statically contains ImageMagick- and IJG-derived code: breadpaper today, and any app that links the whole crate unless the code is dead-stripped. The IJG licence requires that an executable-only distribution state "this software is based in part on the work of the Independent JPEG Group" in its documentation.bread-theme noticecovers the bread-theme binary only. The breadpaper and other consumer packages are stillMITwithlicense_file = "LICENSE". Options: (a) add the IJG/ImageMagick notice to consumers that call the extractor (breadpaper first), (b) put the extractor behind a cargo feature so only opted-in consumers link it, or (c) accept that the crate is no longer plain MIT and document it ecosystem-wide.bread-theme/bakery.tomlhas nolicense_file, and bakery ships only the binary.bread-theme noticecovers the obligation for the CLI. Confirm bakery does not need licence metadata.Acceptance criteria as I read them
tests/fixtures/golden/*,output.rsgolden tests assert every slot and the full list).$XDG_CACHE_HOME/bread/palette.json, with~/.cache/wal/colors.jsonfallback. Met (output.rs palette_cache_path,palette.rs load_palette, with a precedence test).output.rs sequences_string/write_global_palette).Rework happens on the existing branch
feature/native-palette, and PR #36 stays open. Do not open a second PR for these keys. After it lands, the consumer PRs (breadpaper#6, breadpad#9, bos-settings#18) must be repinned frombranch = "feature/native-palette"to the release tag. The merge will delete the branch, and their locked git dependencies would stop resolving.wal -i#30Round-3 update, addressing the second-round review plus the user's licensing decision:
Licensing (user decision: keep the port, isolate behind a feature)
Moved the extractor (
extract.rs,jpeg.rs, fixtures/tests/scripts,NOTICE,LICENSE-ImageMagick,LICENSE-libjpeg-turbo) into a new workspace crate,bread-palette-extract, licensedImageMagick AND IJG(droppedBSD-3-Clause: the ported libjpeg-turbo files belong to its core libjpeg API library, which upstream's own license text says is IJG-only; the Modified BSD terms cover the separate TurboJPEG API/cjpeg/djpeg/build system, none of which is used here).bread-themeis plainMITagain and only depends onbread-palette-extractbehind a new, off-by-defaultextractfeature:palette_from_image,pywal_colors,generate_output, andgenerate-output --image/notice's third-party section are the only things gated (public paths unchanged when the feature is on). The bakery/pacman build of thebread-themebinary now enablesextractand ships a combinedLICENSE; CI runs fmt/clippy/test/build with and without the feature.NOTICEalso now reproduces the one entry of ImageMagick's own upstream NOTICE file that pertains to the ported code (its License's redistribution clause requires this).ECO-30 hardening
#[ignore]d test).image-crate path (png/webp/gif/bmp) now uses the same 2 GiB figure as an explicitimage::Limitsinstead of that crate's undocumented 512 MiB default, which was in fact too small for a real 12000x8000 16-bit RGBA PNG.UnexpectedEof) instead of silently returning wrong-but-plausible colours from the ported decoder's zero-padding past the cut; theimagecrate fallback also errors on it.adw.rs/gtk.rs/layerrules.rs/shell/*/assets/shell/*/theme.tomlto describe bread-theme's own palette, keeping the mentions where pywal-compatibility is the actual point (palette.rs's wal-shapedcolors.jsoncache format, ANSI slot naming heritage).Parity re-verified after the move: 84/84 accents and 243/243 full-list entries exact on the committed fixtures (now
bread-palette-extract/tests/golden.rs), matching the pre-move numbers exactly.CI: green (https://git.breadway.dev/Breadway/bread-ecosystem/actions/runs/215).
wal -i#30Third-round review (round-3 commit
c29a829): two small change requests plus a rebase (release-sensitive repo, not merging)CI was green on
c29a829(run 215). The PR is currently not mergeable: main gained #37, #39, #40, #41.Rebase: straightforward
Tested in a throwaway clone (not pushed). Commits 1 to 5 replay cleanly;
c29a829conflicts only in.forgejo/workflows/check.yml, and only in the header comment: #39's lint-pins paragraph vs this PR's--features extractparagraph. Keep both paragraphs. The step lists merge without conflict: lint-pins runs first, then the existing fmt/clippy/test steps, then your extract steps. On the rebased tree:lint-pins.shpasses (11 manifests, all tag-pinned; bread-palette-extract has no git deps), fmt is clean, clippy-D warningsis clean for the workspace and for-p bread-themewith no features,gtk,extractandgtk,extract, andcargo test --workspacepasses with no features,extractandgtk,extract.What checks out
cargo tree -p bread-theme -e normalwith default features and withgtkshows noimageand nobread-palette-extract. Only breadpaper calls the gated functions (grepped every checkout). A no-extract build of the binary exits 1 with a clear message ongenerate-output --image, andnoticeprints the MIT-only text.--features extractand copy the combinedbread-theme/LICENSE, which bakery.toml names aslicense_file. There is no pacman build of bread-theme (packaging/arch/PKGBUILD builds only bakery), so the "bakery/pacman" wording inbread-theme/Cargo.toml:51andCHANGELOG.md:65should just say bakery.bread-themebinary (generate-output --image, isolated XDG dirs) matches realwal -i -n -qin an isolated HOME on color1..6 for 21/21. The 22nd file is a 0-byte .gif that both sides reject.compare-pywal.py --fullgives 336/336 exact.ImageMagick AND IJGis correct. libjpeg-turbo's LICENSE.md limits the Modified BSD licence to the TurboJPEG API, cjpeg/djpeg, libspng and the build system. Every file ported here (jidctint, jdsample, jdcolor, jdmaster, jdmainct, jdapimin, jdhuff/jdphuff) is part of the libjpeg API library, whose headers point only at README.ijg. The NOTICE carries the IJG sentence and the relevant ImageMagick NOTICE entry. Nit: IJG clause 1 literally asks for "this README file" in source distributions. You reproduce only its LEGAL ISSUES section, which is common practice but not verbatim compliance.Change requests
bread-palette-extract/src/jpeg.rs:423-425,src/extract.rs:201-237). When the port rejects a header for the budget,decodereturnsNonewithtruncated == false, soSource::openfalls through to theimagecrate.image::Limits::max_alloconly bounds that decoder's output buffer. I measured the bos-default.jpg fixture (22 KB) with its SOF patched to 20000x20000: 1.9 GB peak RSS, 21 s of CPU, exit 0, and a palette made of garbage. At 14000x14000 it was 0.94 GB and 8.6 s. Make the budget verdict a distinct outcome, liketruncated_out, and haveSource::openreturn anInvalidData"exceeds decode budget" error instead of falling back. Extendhuge_sof_dimensions_are_rejected_before_allocating(jpeg.rs:1146) into aSource::open/wal_colorslevel test with a mid-size lie (for example 20000x20000 with real scan data) that assertsErr. The current test asserts onlyis_none(), which would also pass if a lazily zeroed allocation had happened first.cargo test -p bread-themeruns 118 tests with or without--features extract. The oldgolden_fixture_testscalledcrate::palette_from_image. The newbread-palette-extract/tests/golden.rs:58-62,129-133re-implements the[8..14]slice itself, so the actual consumer API is untested:output.rspalette_from_image, including itsNone => default_accents()fallback. That leaves CI's newtest (extract)step with nothing extra to run for bread-theme. Add a#[cfg(all(test, feature = "extract"))]module inoutput.rsthat runspalette_from_imageon one fixture (path viaCARGO_MANIFEST_DIR/../bread-palette-extract/tests/fixtures) and asserts its golden accents, plus one image under 16 colours that asserts the default accents.Nits (non-blocking)
upsample's full-size per-component outputs (full, jpeg.rs:531-535), which are live alongsideplanesand thenrgb. Real peak is roughly budget plus 3wh. Either count them or reword the "all three buffers" doc.truncated. A scan cut short by a marker still zero-fills silently. This matches libjpeg/ImageMagick behaviour (real wal also exits 0 on such a file), so it is fine, but the CHANGELOG sentence "a JPEG truncated mid-scan now fails outright" is broader than the code.bread-theme/LICENSEis a hand-maintained copy of bread-palette-extract's NOTICE plus both licences. Consider concatenating them in the release workflows so they cannot drift.extract.rs,bread-theme/tests/fixtures/andscripts/compare-pywal.pyunder bread-theme.output.rs:195has the same stale script path.palette_from_image/generate_outputbehind a feature breaks the API for any caller. Tag this as a minor bump (v0.8.0), not a patch.Acceptance criteria (my reading, round 3)
extractis on: met (Cargo.toml changes, tree check above).extractand ships the third-party terms: met (the three workflows).Rework happens on the existing branch
feature/native-palette, and PR #36 stays open. Do not open a second PR. Rebase onto main as described above when pushing the rework.wal -i#30c29a829b804c51998e0cRound-4 update, addressing the third-round review (#issuecomment-1377):
jpeg.rs,extract.rs).jpeg::decodenow reports an over-budget SOF header through a distinctover_budget_outflag, separate fromtruncated_outand from a plain unsupported-featureNone.Source::openturns that into a hardio::ErrorKind::InvalidDataerror instead of falling back to theimagecrate, which was decoding the same oversized claim a second time with its own unbounded allocator. Added awal_colors-level regression test with a real JPEG whose SOF lies about a 20000x20000 image (real scan data behind it, not just a crafted header), assertingErrquickly rather than justdecode(..).is_none().3 * width * heightbytes past the documented budget.output.rsgained anextract_feature_testsmodule that calls the realpalette_from_imagepublic function (notbread-palette-extract's internal slice) against the golden fixtures, the default-accent fallback for a sub-16-colour image, and a decode error.bread-theme/LICENSE(the file bakery actually ships) still carries the ImageMagick and libjpeg-turbo/IJG licence texts verbatim, so the hand-maintained combined file can't silently drift from its sources.bread-theme/scripts/bread-theme/tests/fixturespaths in CHANGELOG andoutput.rsnow that the extractor lives inbread-palette-extract, and clarified that only an EOF-without-marker truncation fails outright (a scan cut short at a marker boundary still zero-fills, matching libjpeg-turbo/ImageMagick)..forgejo/workflows/check.yml's header comment (lint-pins paragraph vs the--features extractparagraph); kept both, in the order you confirmed (lint-pins, then fmt/clippy/test, then extract steps).Verified on the rebased tree:
bash scripts/lint-pins.sh(11 manifests, tag-pinned),cargo fmt --all -- --check,cargo clippy --workspace --all-targets --locked -- -D warningswith no features,gtk,extract, andgtk,extract,cargo test --workspace --lockedfor the same four combinations (all green, including the new tests), andcargo build --workspace --lockedwith and withoutextract. Force-pushed tofeature/native-palette; CI run is in progress.