Skip to content

clip-regions feature - #340

Open
fsarud wants to merge 3 commits into
jperon:masterfrom
fsarud:feat-select-measures
Open

clip-regions feature#340
fsarud wants to merge 3 commits into
jperon:masterfrom
fsarud:feat-select-measures

Conversation

@fsarud

@fsarud fsarud commented Jul 14, 2026

Copy link
Copy Markdown

Description

This PR adds native support for precise, measure-based score clipping using LilyPond's -dclip-systems backend. This is my first PR ever, so I kindly ask for some indulgence regarding any lack of experience on my part.

Previously, users had to rely on print-only, which compiles the entire score and filters pages. By bypassing lilypond-book-preamble.ly's default book-handler specifically for clips, we are able to unlock LilyPond's native vector clipping system.

Usage

\lilypondfile[clip-regions=a-b]{file.ly}
where a and b are the measures of the \score block in file.ly that you wish to clip.

The clip is forced to begin a new system, so sequential commands like \lilypondfile[clip-regions=a-b]{file.ly} and \lilypondfile[clip-regions=c-d]{file.ly} (with a<>c or b<>d) will generate two different hashes and compile completely independently of one another .

Usage with ragged-right=true is highly recommended.

Changes Introduced

  1. Preamble Handler Override: Intercepted lilypond-book-preamble.ly injection inside Score:header() to append #(define default-toplevel-book-handler print-book-with-defaults), allowing -dclip-systems to work.
  2. Engraver Injection: Automated the addition of \consists \Clip_break_engraver and clip-regions mapping inside the generated \layout block.
  3. CLI Flag: Conditional insertion of the -dclip-systems flag in Score:lilypond_cmd() only when clip-regions is used.
  4. Deterministic Indexing: Reworked set_lyscore and latex_includesystems to dynamically parse, sort, and inject the newly generated *-clip.pdf. clip-regions use the .pdf, not the .eps. This logic relies directly on the compiled .pdf vectors instead of the .eps files, and the latter are cleaned up to optimize space.

Verification

Tested on Arch Linux using TeX Live 2026 and Lilypond 2.26.0. Works for both single-system and multi-system clips.

@MacLotsen

Copy link
Copy Markdown
Collaborator

Thanks for your contribution. Can you translate variable names and comments to English?

Also, to test it now with CI, you can add a run for your test file in the make file under target test.

@fsarud

fsarud commented Jul 23, 2026

Copy link
Copy Markdown
Author

Done! Hope it is correct.

Comment thread lyluatex.lua Outdated
-- fsarud: if clip-regions is set:
if score['clip-regions'] and score['clip-regions'] ~= '' then
-- Override indent
local hoffset = score.protrusion or 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think you should use potrusion_left as was just fixed now in #337
score.potrusion is always 0, I guess

Comment thread lyluatex.lua Outdated
for i = 1, score.nsystems do table.insert(score.range, i) end

-- Remove all intermediate .eps files associated with this hash
pcall(function()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing intermediate files should be done by the build system (i.e. latexmk or make), not in the score-setup phase. It kills caching and swallows errors. There's also Score:delete_intermediate_files() already

Comment thread lyluatex.lua
Comment thread lyluatex.lua
@@ -863,6 +993,13 @@ function Score:lilypond_cmd()
local cmd = shell_quote(self.program) .. ' '
.. (self.insert == "fullpage" and "" or "-E ")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To avoid EPS runs, we must ommit the option -E:

((self.insert == "fullpage" or clip_regions) and "" or "-E ")

And before make the bool available:

local clip_regions = self['clip-regions'] and self['clip-regions'] ~= ''

@MacLotsen

Copy link
Copy Markdown
Collaborator

There are still some Spanish comments left and you can also omit the author tags (we use Git blame for that already).

In general on the comments, let's try to avoid generating EPS at all if you don't use it.

Hope it helps :)

@fsarud

fsarud commented Jul 25, 2026

Copy link
Copy Markdown
Author

Hi. I reworked delete_intermediate_files in order to include the *clip*.eps files generated by -dclip-systems (they remain because of -dno-delete-intermediate-files. Also I removed every trace of spanish I found.
My concern remains with protrusion_left. The pdf clips are already «clipped» to the musical content and, I believe, protrusion_left should be always zero if clip-regions is set.

@MacLotsen

Copy link
Copy Markdown
Collaborator

Thanks — this is a genuinely nice piece of work, especially for a first PR. Bypassing the book handler to unlock -dclip-systems is the right idea, and the Clip_break_engraver for forcing the system break is clever.

On your protrusion_left question: you're right, and it's worth more than a zero-assignment. protrusion and friends exist because lyluatex has to guess how far a system sticks into the margin and compensate for it. With clip-regions LilyPond crops each clip to the music itself, so there is nothing to compensate — the whole check should be skipped, not just neutralised afterwards:

function Score:check_protrusion(bbox_func)
    self.range = self:calc_range()
    if self.insert ~= 'systems'
        or (self['clip-regions'] and self['clip-regions'] ~= '')
    then return self:is_compiled() end

There is a hard reason too: bbox_parse() reads %%BoundingBox from the EPS file. Drop the EPS without this guard and it returns nil, so repeat … until self:check_protrusion(bbox_get) in Score:process() never terminates — lualatex spins forever. I hit exactly that while testing.

On the EPS cleanup — the good news is you don't need it at all. The leftover *-clip.eps are not produced by -dclip-systems; they are produced by lyluatex's own -dno-delete-intermediate-files. LilyPond dumps each clipped system as EPS and converts it to PDF (scm/lily/backend-library.scm, clipped-systems-stencils), then deletes the EPS itself — unless we tell it not to. Checked on 2.24.3:

flags leftovers
-dclip-systems -dno-delete-intermediate-files score.eps, …-clip.eps, …-clip.pdf
-dclip-systems …-clip.pdf only

So in clip mode we can pass neither -E nor -dno-delete-intermediate-files, and delete_intermediate_files() can go back to what it was. Output is pixel-identical, and it skips a Ghostscript call per score as a bonus.

One thing that surprised me and deserves a comment in the code: your sort is correct, but it looks wrong. LilyPond walks its system list backwards, so -clip is the last system and -clip-1 the one before it. Anyone reading return n_a > n_b later will "fix" it into a bug.

Smaller things:

  • clip-regions + insert=fullpage fails silently — ly_preamble() omits lilypond-book-preamble.ly there, so the gsub matches nothing and you get "The score doesn't contain any music". Worth a warn().
  • A bad clip-regions value emits a %% comment into the .ly; a warn() would actually reach the user.
  • table.insert(score, …) puts the clip paths as positional entries on the options table, and latex_includesystems then reads ly.score[index] behind its own signature — a Score:clip_files() helper reads better.
  • score.leftgutter = 0 overrides the user's gutter= / leftgutter= option.
  • Still missing for a user-facing option: a section in lyluatex.md, a CHANGES entry, and the inclusive-range convention documented (2-14 = measures 2–14). And please add yourself to Contributors.md.
  • The .gitignore entries for lyluatex.code-workspace / lyluatex.lua.old are editor-local — better in your global gitignore.

About the conflict: your branch is fine, it just predates the current master. A plain git rebase master applies cleanly, and it makes the unrelated version/copyright hunks disappear. Note that master now has the #283 fix (protrusion_left instead of protrusion in set_lyscore) — that is the one spot you will have to resolve.

I have the rebase plus the EPS change on a branch here if you would like me to push it somewhere you can pull from; happy to leave it to you either way. Nice feature — looking forward to having it in.

@fsarud

fsarud commented Jul 27, 2026

Copy link
Copy Markdown
Author

Thanks a lot! Yours is really a detailed and insightful review!

Bypassing check_protrusion for clips and omitting both -E and -dno-delete-intermediate-files makes total sense. That was my first attempt but, as you say, omitting both parameters goes directly into an infinite loop. I felt that messing with protrusion calculations were something risky so I opted for the more conservative approach: leave -dno-delete-intermediate-files and manually delete the intermediate files that clip-regions created. Your solution is the one I originally looked for.

On the sort: yes, the way lilypond produces the clips it is rather unusual: first the numbered one(s) in decreasing order, then the unnumbered one. Worth a comment, of course.

About the rebase: since you already have the rebase and the EPS adjustment ready on your branch, please feel free to push it to this PR! Then, I will gladly add the remaining user-facing items: lyluatex.md, CHANGES, error handling with warn(), the helper Score:clip_files(), and adding myself to Contributors.md.

Thanks again for your guidance and kindness!

@MacLotsen MacLotsen mentioned this pull request Jul 28, 2026
@MacLotsen

MacLotsen commented Jul 28, 2026

Copy link
Copy Markdown
Collaborator

I can merge it into main (see #341), but for your fork, you'll have to force reset it to jperon:master, I'm afraid, or jperon:clipping-regions if not merged already.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants