Make a bundle reopenable, and number slices
A bundle was a one-way trip. The slice images are output and the cuts that produced them lived only in the producer's own project file, so a bundle someone handed you meant cutting the score again from scratch. The manifest now carries the geometry, in a `source` block: per page the cut polylines, skew, levels and content rectangle, all in normalised coordinates so they survive any render resolution, and per slice the page and slot it came from. Discards are stated by omission — a slot no slice claims was discarded — since shipping a discarded slice's image would defeat discarding it. `noteman-slicer open song.zip` unpacks the archived PDF, rebuilds the project from that geometry, restores markers, engravings and the title block, and opens the editor. Jump destinations go back from an array index to the (page, slot) the editor works in. The images in the zip are discarded: the PDF is what the pipeline renders from. Re-exporting a reopened bundle reproduces its manifest exactly. It refuses to overwrite a PDF or project file already sitting there, because the obvious place to unpack is where someone's unfinished cuts live. Separately, every slice can now carry the measure it starts at, not just a re-engraved one — a scanned system is numbered in the score the same way, and noteman wants to answer "take it from bar 33" about either. It moves off the replacement onto the page, alongside markers and discards, and out of the bundle's engraving object onto the slice.
This commit is contained in:
+79
-5
@@ -95,6 +95,7 @@ which roughly 830 KB is the source PDF and the rest slice images at ~20 KB each.
|
||||
| `translator` | string | optional | Who translated the words. |
|
||||
| `tempo` | integer | optional | Beats per minute. |
|
||||
| `voices` | string | optional | The parts in this arrangement, as free text. |
|
||||
| `source` | object | optional | How the slices were cut from the archived document. See [Source geometry](#source-geometry). |
|
||||
|
||||
**Optional fields are omitted when they have no value.** A consumer will not
|
||||
encounter an empty string or a null in place of an absent field.
|
||||
@@ -112,6 +113,9 @@ Each entry of `slices` is an object:
|
||||
| Field | Type | | |
|
||||
|---|---|---|---|
|
||||
| `file` | string | required | Name of the image entry in the archive. |
|
||||
| `page` | integer | optional | Index into `source.pages` — the page this slice was cut from. Present whenever `source` is. |
|
||||
| `slot` | integer | optional | Which slice of that page this is, counting from 0 between its cuts. Present whenever `source` is. |
|
||||
| `bar` | integer | optional | The measure this slice starts at, as numbered in the score. Omitted when unknown. |
|
||||
| `markers` | array | optional | Markers on this slice. Omitted when there are none. |
|
||||
| `engraving` | object | optional | The notation this slice's image was engraved from, when it was engraved rather than scanned. See [Engraving](#engraving). |
|
||||
|
||||
@@ -122,6 +126,12 @@ must not derive order from them.
|
||||
A slice's **index** is its zero-based position in this array. Indices are the
|
||||
only identifiers the format has, and they are meaningful only within one bundle.
|
||||
|
||||
`bar` is the score's own numbering, not the format's: it says which measure this
|
||||
system begins at, so a consumer can answer "take it from bar 33" by scrolling to
|
||||
the right slice. It is independent of `index`, may be absent on any slice, and
|
||||
carries no promise of being consecutive — a score numbers the systems it chooses
|
||||
to, and pickup bars, repeats and voltas all break arithmetic on it.
|
||||
|
||||
## Slice images
|
||||
|
||||
Every slice image in a bundle satisfies the following. A consumer can rely on
|
||||
@@ -160,6 +170,64 @@ One specific hazard is worth naming, because it is silent: an image pipeline
|
||||
that *discards* the alpha channel rather than compositing it will turn every
|
||||
slice into a solid black rectangle, since the colour channels are all zero.
|
||||
|
||||
## Source geometry
|
||||
|
||||
A bundle can say how its slices were cut, in a `source` object. With it a
|
||||
consumer can reopen the score for editing; without it the bundle is a one-way
|
||||
trip, since the slice images are output and the decisions that produced them
|
||||
would live only in whatever tool made them.
|
||||
|
||||
```json
|
||||
"source": {
|
||||
"file": "original.pdf",
|
||||
"pages": [
|
||||
{
|
||||
"skew": -0.4,
|
||||
"content": [0.083, 0.0, 0.947, 1.0],
|
||||
"levels": [46, 173],
|
||||
"cuts": [
|
||||
[[0.0, 0.0449], [1.0, 0.0449]],
|
||||
[[0.0, 0.3662], [0.35, 0.3662], [0.35, 0.3901], [1.0, 0.3901]]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
| Field | Type | | |
|
||||
|---|---|---|---|
|
||||
| `file` | string | required | The archive entry the slices were cut from. `"original.pdf"` in practice. |
|
||||
| `pages` | array | required | One entry per page of that document, in its own page order. |
|
||||
|
||||
Each entry of `pages`:
|
||||
|
||||
| Field | Type | | |
|
||||
|---|---|---|---|
|
||||
| `cuts` | array | required | The boundaries between slices, ordered top to bottom. May be empty: a page with no cuts is one slice. |
|
||||
| `skew` | number | optional | Degrees the page was rotated by before cutting. Default `0`. |
|
||||
| `content` | array | optional | `[x0, y0, x1, y1]` — the part of the page that is music. Default the whole page. |
|
||||
| `levels` | array | optional | `[black, white]` — the black and white points applied. Default `[0, 255]`. |
|
||||
|
||||
**Everything here is in normalised page coordinates**, `0.0` to `1.0` on each
|
||||
axis, origin top-left. Nothing is in pixels, so the geometry holds however the
|
||||
document is rendered and at whatever resolution.
|
||||
|
||||
A **cut** is a polyline: a list of `[x, y]` points, left to right. Two points is
|
||||
a straight cut; more steps around a system that interleaves with its neighbour —
|
||||
a section label printed level with the previous system's lyrics. A slice's top
|
||||
boundary is the cut above it and its bottom boundary the cut below it, with the
|
||||
page edge standing in at either end.
|
||||
|
||||
A page with *n* cuts therefore has *n + 1* **slots**, numbered from 0 downward.
|
||||
Each slice names the `page` and `slot` it came from. **A slot that no slice
|
||||
claims was discarded** — a page header, a footer, a title block. That is stated
|
||||
by omission rather than directly, because shipping a discarded slice's image
|
||||
would defeat discarding it.
|
||||
|
||||
The archived document is the source of truth for reopening: the slice images are
|
||||
output, and a consumer that reopens a bundle re-renders them rather than
|
||||
importing them.
|
||||
|
||||
## Engraving
|
||||
|
||||
Most slices are photographs of print: an image and nothing more. A slice that
|
||||
@@ -169,12 +237,12 @@ it came from, in an `engraving` object.
|
||||
```json
|
||||
{
|
||||
"file": "007.webp",
|
||||
"bar": 33,
|
||||
"engraving": {
|
||||
"lang": "lilypond",
|
||||
"key": "aes",
|
||||
"time": "4/4",
|
||||
"print_time": false,
|
||||
"bar": 33,
|
||||
"voices": [
|
||||
{ "clef": "treble", "notes": "c4 des ees f | ees2. r4", "lyrics": "Kai -- paa -- va sy -- dän" },
|
||||
{ "clef": "treble_8", "notes": "aes,4 aes aes aes | aes2. r4" },
|
||||
@@ -191,7 +259,6 @@ it came from, in an `engraving` object.
|
||||
| `key` | string | optional | Key signature, in `lang`'s spelling. For `lilypond`, the tonic of the major spelling: `"aes"`, `"c"`, `"fis"`. |
|
||||
| `time` | string | optional | Time signature, as `"4/4"`. |
|
||||
| `print_time` | boolean | optional | Whether the time signature is printed on this system. Default `false`. |
|
||||
| `bar` | integer | optional | The measure this system starts at, as printed above its first bar. Omitted when the system is not numbered. |
|
||||
|
||||
Each entry of `voices`:
|
||||
|
||||
@@ -288,9 +355,9 @@ ignore markers it does not understand rather than reject the bundle.
|
||||
|
||||
`v` is an integer that increases when a change would break an existing consumer.
|
||||
Additions that a consumer can safely ignore — new optional fields, new marker
|
||||
types, new `engraving` languages — do not increase it. `engraving` was added
|
||||
this way: a bundle carrying one is still a version 1 bundle, and a consumer that
|
||||
has never heard of it presents the score unchanged.
|
||||
types, new `engraving` languages — do not increase it. `engraving` and `source`
|
||||
were both added this way: a bundle carrying them is still a version 1 bundle,
|
||||
and a consumer that has never heard of them presents the score unchanged.
|
||||
|
||||
A consumer should refuse a bundle whose `v` it does not recognise rather than
|
||||
attempt to interpret it.
|
||||
@@ -305,6 +372,9 @@ A consumer is advised to check:
|
||||
- Every `destination` is within the bounds of `slices`.
|
||||
- Every `engraving` has a `lang` and a non-empty `voices`; unknown `lang` values
|
||||
are ignored rather than rejected.
|
||||
- If `source` is present: its `file` names an entry in the archive, every slice
|
||||
carries a `page` within `source.pages` and a `slot` within that page's slot
|
||||
count, and no two slices claim the same one.
|
||||
- Archive entry names contain no path separators, no `..`, and no absolute
|
||||
paths, as with any archive from an untrusted source.
|
||||
|
||||
@@ -314,6 +384,10 @@ A bundle describes one complete score. The format has no notion of updating a
|
||||
previously read bundle: there are no stable identifiers, and a slice's index is
|
||||
meaningful only within the bundle that contains it.
|
||||
|
||||
Reopening a bundle for editing, via [`source`](#source-geometry), does not change
|
||||
that. What comes out is a new document that happens to have been derived from an
|
||||
old one, not a revision of it.
|
||||
|
||||
Two bundles of the same piece are therefore independent documents, not versions
|
||||
of one. A consumer that stores imported bundles and assigns its own identifiers
|
||||
should treat a second bundle as a new score rather than merging it into an
|
||||
|
||||
Reference in New Issue
Block a user