Repository navigation
refactor: image edit registry, with Invert as its first edit - #552
Merged
Merged
Conversation
Workpieces carry an ordered stack of image edits. The ops come from a new image edit registry that addons fill through the register_image_edits hook (plugin API 22); the core only applies them and skips ops that are not registered. get_vips_image() becomes the private _get_vips_image() for the unedited render, and get_processed_image() applies the stack. Every consumer of the workpiece's pixels goes through it, so the canvas, the 3D preview and the pipeline see the same image. Unregistered ops are reported through the existing missing-features dialog, and steps can hand legacy settings over to the stack after a document loads. Refs barebaric#515
The addon registers an Invert edit that inverts the colour and keeps alpha, and a Tools > Invert Image action that toggles it on the selected workpieces as one undo step. Refs barebaric#515
The Engrave step no longer has an Invert setting, and the raster preprocessing no longer knows about inversion. Projects saved with Invert on an Engrave step get the Invert edit on the workpieces of that layer when they are opened. Refs barebaric#515
# Conflicts: # rayforge/core/hooks.py
The addon now registers its action directly in the register_actions hook with an enabled_when predicate, instead of deferring through action_extension_registry. Plugin API is 23 after merging main's API 22.
The Engrave step lost its Invert row to the new image edit registry, and the dither and post processing pages picked up the Serpentine and Crop to Stock rows from main.
Embed a before/after example pair for Invert Image, generated by scripts/media/gen_invert_example.py, and translate the image editing addon page plus the Invert paragraphs of the engrave page into all shipped website languages.
Contributor
|
Thanks a lot for the refactoring PR! I made some refinements and think this makes main ready for rebasing #535 on top of it! |
This was referenced Oct 13, 2026
This branch was previously deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This is the architecture-only refactor you asked for on #535: an image edit registry, with Invert turned into its first edit and removed from the Engrave step. No new editing tools; #535 gets reworked on top of this once it lands.
Shape
Core (
rayforge/core/image_edit/). The core only knows the registry, never what an edit does.ImageEditStep(op, params, enabled): one entry in a workpiece's edit stack. It is frozen and JSON-serializable. Keys it does not know are kept and written back.ImageEditOp: the base class an addon implements (NAME,LABEL,apply(image, params) -> pyvips.Image).ImageEditRegistry/image_edit_registry: built likeStepRegistry, includingunregister_all_from_addon.apply_image_edits(): applies the enabled edits in order and skips ops that are not registered.missing_image_edit_ops()lists those ops.register_image_edits(image_edit_registry), registeredworker_ok=Truebecause the pipeline renders. Plugin API goes to 22, with a changelog entry inhooks.py.WorkPiece.
image_edits: a tuple ofImageEditStep. The setter clears the processed-image cache and sendsupdated, so the pipeline and the canvas pick up a change the same way they do forsource_segment.to_dict/from_dict(older files load with no edits) and copied inin_world()._get_vips_image()(private) is the unedited render. It is the oldget_vips_image()unchanged, still on top of the shared SourceAsset base cache.get_processed_image()(public) applies the stack and caches the result per size.get_processed_image():render_to_pixels,render_chunkand the 3D preview. Everything that consumes pixels goes throughrender_to_pixels, so the canvas, Engrave, auto-levels, the levels histogram, tracing fallbacks and Print & Cut all see the edited image.Image Editing addon (
rayforge-addon-image-editing).InvertOp(NAME = "invert"). The frontend adds Tools > Invert Image.InvertOpinverts the colour bands and leaves alpha alone, so transparent areas stay transparent and are still not engraved.Missing addon.
Doc.missing_image_edit_opsjoinsmissing_step_types, so a project that uses an edit whose addon is unavailable gets the existing missing features dialog. As you suggested, the edit is then skipped (and kept in the file).Removing Invert from the Engrave step
The step loses the setting, its row, its cache key and its
to_dictentry. The levels histogram and the Power section's min/max titles no longer depend on it.Older projects.
Step.pop_legacy_image_edits()returns[]on the base class.EngraveStepreturns[invert]if the file hadinvert: true, and only once.Doc.from_dict,Doc.adopt_legacy_image_edits()appends those edits to the workpieces of the step's layer and skips any edit a workpiece already has. That makes it idempotent, which the addon-toggle round trip inDocEditorneeds.invertstays a known key onEngraveStep, so an old value does not end up inextraand get written back forever.How faithful this is:
gray > threshold), so at the default threshold of 254 an inverted step burned only pure white. Now it burns everything that is not near-black, the mirror image of the non-inverted output. I think that is what Invert was meant to do, but it is a change.invertvalue, as for any key that leavesrecipe_keys().Not in this PR
TraceSpec.invert(the import dialog's Invert for tracing) is untouched. It could become the same edit later, but that changes what the import dialog stores, so I left it out of an architecture PR..po/.potchanges and only English docs:addons/image-editing.md(new) plus the two Invert paragraphs inengrave.md.Verification
image_edit(13), WorkPiece edits (11), Doc adoption (6), image-editing addon (11, including two tests that pin the old Invert behaviour for dither and power modulation), laser addon (189), and the laser UI pages (54, run with-m ui).image_editsbecomes["invert"].invert: trueon its Engrave step loads with the workpiece inverted, with no missing-feature dialog and noinvertleft on the step.