Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ The **Film Strip** under the canvas shows the open frames in one row. Drag its t

The toolbar's **Roll** row holds:

* The roll's name and its count of frames, keepers and rejects.
* The roll's name and count of frames, keepers and rejects.
* The **filter box**: type part of a file name, or terms such as `film:portra iso:>=400` (below). The search button, or **Enter**, searches the whole library.
* **Sort**: by name, date or scene.
* **Sort**: name, date, scene or frame number.
* **Sheet filter**: all frames, keepers only, hide rejected, or unmarked only. The button is highlighted while a filter hides frames.
* **Roll Settings** (the tag): gear and film details for this frame, the selection or the roll.
* **Apply**: copies chosen settings of this frame to the selection or the roll.
Expand Down Expand Up @@ -711,7 +711,7 @@ On the **Alongside Exports** tab. A contact sheet shows the whole roll as film s
* **Contact Sheet…**: opens the sheet. Drag an edge of the paper to resize it.
* **Film format**: 35mm, 35mm Half Frame or 120 (with **Frame size** for 120).
* **Paper**, **Width**, **Height** and **DPI**: the sheet's size and resolution. The default paper holds a 36-exposure roll.
* **Order**: by file name, by date, or by scene with a new strip or row for each scene.
* **Order**: by file name, by date, by roll and frame number, or by scene with a new strip or row for each scene.
* **Print**: **As Edited**, or **Straight Proof**: the whole roll at one exposure and normal contrast, so you see which negatives are thin or dense.
* **Pick Frames**: click a frame to leave it out or put it back. Rejects start out.
* **Edge Print**: the film maker's markings along the edges, such as the film name, frame numbers and the colored lines a stock prints by its perforations.
Expand Down
13 changes: 11 additions & 2 deletions negpy/desktop/session.py
Original file line number Diff line number Diff line change
Expand Up @@ -526,7 +526,7 @@ def __init__(self, state: AppState, facts_provider: Optional[Any] = None):
# Returns {asset hash: facts}. Without one, plain queries see file facts only
# (name, ext, date), which is all a model built outside a session can know.
self._facts_provider = facts_provider
self._sort_order = "name" # "name" | "date" | "scene"
self._sort_order = "name" # "name" | "date" | "scene" | "frame"
self._sort_descending = False
self._filter_text: str = ""
self._filter_regex: bool = False
Expand Down Expand Up @@ -561,6 +561,9 @@ def _rebuild_indices(self) -> None:
order = self.effective_sort_order
if order == "date":
indices.sort(key=lambda i: _asset_mtime(files[i]), reverse=self._sort_descending)
elif order == "frame":
facts = self._facts_provider() if self._facts_provider else {}
indices.sort(key=lambda i: self._frame_rank(files[i], facts), reverse=self._sort_descending)
else:
indices.sort(key=lambda i: files[i]["name"].lower(), reverse=self._sort_descending)
if order == "scene":
Expand Down Expand Up @@ -642,7 +645,7 @@ def filter_text(self) -> str:
return self._filter_text

def set_sort_order(self, order: str) -> None:
if order not in ("name", "date", "scene"):
if order not in ("name", "date", "scene", "frame"):
order = "name"
self._sort_order = order
self._apply_reindex()
Expand Down Expand Up @@ -670,6 +673,12 @@ def _scene_rank(self, file_info: dict) -> tuple:
return (1, 0)
return (0, -scene[0] if self._sort_descending else scene[0])

def _frame_rank(self, file_info: dict, facts: dict) -> tuple:
"""Frame No. sort: the frame's metadata Roll, then its Frame No., the name a tie."""
f = facts.get(file_info["hash"]) or {}
num = f.get("frame")
return (f.get("capture_roll", ""), num if num is not None else 0, file_info["name"].lower())

def scene_runs(self) -> list[tuple[Optional[int], int, int]]:
"""``(scene ordinal or None, first row, last row)`` for each scene's block of display
rows, frames in no scene as the last block. Empty unless sorted by Scene."""
Expand Down
8 changes: 6 additions & 2 deletions negpy/desktop/view/sidebar/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -1517,9 +1517,12 @@ def _init_ui(self) -> None:

# The frames' own order; the Library's roll list has a Sort of its own.
self.sort_btn = SortButton(
(("name", tr("Name")), ("date", tr("Date")), ("scene", tr("Scene"))), tr("Sort the frames in the Film Strip")
(("name", tr("Name")), ("date", tr("Date")), ("scene", tr("Scene")), ("frame", tr("Frame No."))),
tr("Sort the frames in the Film Strip"),
)
self.act_sort_name, self.act_sort_date, self.act_sort_scene, self.act_sort_frame = (
self.sort_btn.order_action(k) for k in ("name", "date", "scene", "frame")
)
self.act_sort_name, self.act_sort_date, self.act_sort_scene = (self.sort_btn.order_action(k) for k in ("name", "date", "scene"))
self.act_sort_asc, self.act_sort_desc = self.sort_btn.ascending_action, self.sort_btn.descending_action
self.act_sort_scene.setVisible(False)
self.sort_btn.order_selected.connect(self._apply_sort_order)
Expand Down Expand Up @@ -2089,6 +2092,7 @@ def _sync_sort_menu(self) -> None:
self.act_sort_name.setChecked(order == "name")
self.act_sort_date.setChecked(order == "date")
self.act_sort_scene.setChecked(order == "scene")
self.act_sort_frame.setChecked(order == "frame")

def _apply_sheet_filter(self, mode: str, save: bool = True) -> None:
model = self.session.asset_model
Expand Down
9 changes: 6 additions & 3 deletions negpy/desktop/view/widgets/contact_sheet_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
SheetLook,
StraightProof,
creation_order,
frame_order,
print_aspect,
scene_breaks,
scene_of,
Expand All @@ -66,8 +67,8 @@
_CUSTOM_PAPER = len(ILFORD_PAPERS)
_PRINTS = (tr("As Edited"), tr("Straight Proof"))
_AS_EDITED, _STRAIGHT_PROOF = 0, 1
_ORDERS = (mark("Name"), mark("Date"), mark("Scene"))
_BY_NAME, _BY_DATE, _BY_SCENE = 0, 1, 2
_ORDERS = (mark("Name"), mark("Date"), mark("Scene"), mark("Frame No."))
_BY_NAME, _BY_DATE, _BY_SCENE, _BY_FRAME = 0, 1, 2, 3
_EDITED_TILES, _PROOF_TILES, _SCENE_PROOF_TILES = 0, 1, 2
_CANVAS_PAD = 28.0
_TILE_REPAINT_MS = 120
Expand Down Expand Up @@ -415,7 +416,7 @@ def _build_controls(self, settings: ContactSheetSettings) -> QWidget:

self.order_btn = StepCombo.of(
[tr(order) for order in _ORDERS],
tr("Lay the frames out by file name, in the order they were taken, or with a new strip for each scene."),
tr("Lay the frames out by file name, by the order they were taken, by roll and frame number, or by scene."),
)
self.order_btn.set_item_enabled(_BY_SCENE, self._has_scenes)
if not self._has_scenes:
Expand Down Expand Up @@ -651,6 +652,8 @@ def _order_indices(self, order: int) -> list[int]:
return scene_order(self._frames)
if order == _BY_DATE:
return creation_order(self._frames)
if order == _BY_FRAME:
return frame_order(self._frames)
return list(range(len(self._frames)))

def _on_order_changed(self, index: int) -> None:
Expand Down
2 changes: 2 additions & 0 deletions negpy/services/assets/search.py
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,8 @@ def facts_for(asset: dict, config: Any = None) -> dict[str, Any]:
"format": (meta.format_other if meta.format == "Other" else meta.format).casefold(),
"scanning": meta.scanning.casefold(),
"roll": " ".join(p for p in (meta.capture_roll, config.process.roll_name or "") if p).casefold(),
# Raw Roll value, not in FIELDS: the Frame No. sort orders by it, not by the combined search term.
"capture_roll": meta.capture_roll.casefold(),
"frame": meta.capture_frame,
"iso": meta.film_iso,
"push": meta.push_pull,
Expand Down
2 changes: 1 addition & 1 deletion negpy/services/export/contact_sheet_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ def frame_turns(upright_aspect: Optional[float], geometry: FilmGeometry, rotatio
return 3


SHEET_ORDERS = ("name", "date", "scene")
SHEET_ORDERS = ("name", "date", "scene", "frame")


@dataclass(frozen=True)
Expand Down
11 changes: 11 additions & 0 deletions negpy/services/export/contact_sheet_roll.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ def key(index: int) -> tuple[Any, ...]:
return sorted(range(len(frames)), key=key)


def frame_order(frames: Sequence[SheetFrame]) -> list[int]:
"""Indices by the frame's Roll and Frame No. from its metadata, the name a tie."""

def key(index: int) -> tuple[Any, ...]:
meta = frames[index].config.metadata
num = meta.capture_frame
return (meta.capture_roll.casefold(), num if num is not None else 0, frames[index].name.lower())

return sorted(range(len(frames)), key=key)


def _crop_fractions(config: WorkspaceConfig) -> tuple[float, float]:
rect = config.geometry.crop_rect
if not rect:
Expand Down
8 changes: 4 additions & 4 deletions negpy/translations/USER_GUIDE.de.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Eine Ordnerrolle enthält nur die Dateien ihres Ordners. Eine Datei, die du aus

Um die Bilder im Filmstreifen als Rolle zu speichern, nutze den Filmstreifen. **In Ordnerrolle speichern** macht aus Bildern, die alle aus einem Ordner stammen, die Rolle dieses Ordners. Seine anderen Dateien erscheinen als entfernt. **Als virtuelle Rolle speichern…** behält genau diese Bilder, aus beliebigen Ordnern. In Ordnerrolle speichern macht auch aus einer virtuellen Rolle eines Ordners die Rolle dieses Ordners. Die neue Rolle startet mit den Einstellungen, den Kartensperren und den nur in dieser Rolle gemachten Bearbeitungen der geöffneten Rolle. Ein Bild hat in jeder Rolle, die es enthält, dieselbe Bearbeitung. Rechtsklick → **In dieser Rolle unabhängig bearbeiten** gibt ihm in einer Rolle eine eigene Bearbeitung.

<!-- panel:frames src:e18d0aea -->
<!-- panel:frames src:c1f54a29 -->
### Filmstreifen

Der **Filmstreifen** unter der Arbeitsfläche zeigt die geöffneten Bilder in einer Reihe. Ziehe seinen oberen Rand, um die Bilder zu vergrößern.
Expand All @@ -27,7 +27,7 @@ Die Zeile **Rolle** der Werkzeugleiste enthält:

* Den Namen der Rolle und die Anzahl der Bilder, der behaltenen und der verworfenen Bilder.
* Das **Filterfeld**: Gib einen Teil eines Dateinamens ein oder Begriffe wie `film:portra iso:>=400` (siehe unten). Die Suchschaltfläche oder **Enter** durchsucht die ganze Bibliothek.
* **Sortieren**: nach Name, Datum oder Szene.
* **Sortieren**: nach Name, Datum, Szene oder nach Rolle und Bildnummer.
* **Bogenfilter**: alle Bilder, nur behaltene, verworfene ausblenden oder nur unmarkierte. Die Schaltfläche ist hervorgehoben, solange ein Filter Bilder ausblendet.
* **Rolleneinstellungen** (das Etikett): Ausrüstung und Filmdetails für dieses Bild, die Auswahl oder die Rolle.
* **Anwenden**: kopiert gewählte Einstellungen dieses Bildes auf die Auswahl oder die Rolle.
Expand Down Expand Up @@ -505,7 +505,7 @@ Zusätzliche Dateien neben dem Abzug. Jede hat links einen Schalter und rechts e
* **Auto-Dichte / Gradation**: **Beim Export festschreiben** fixiert die automatische Helligkeit und den Kontrast jedes exportierten Bildes, damit sie sich später nicht ändern. **In Rolle festschreiben** tut das jetzt für jedes sichtbare Bild.
* **Bearbeitungs-Sidecar-Dateien**: **Beim Export speichern** schreibt neben jede Quelle eine `.negpy`-Datei mit der Bearbeitung. **Sidecar-Dateien exportieren** schreibt sie für alle sichtbaren Bilder. Rechtsklick auf ein Bild → **Bearbeitung aus Sidecar-Datei laden…** liest eine zurück.

<!-- panel:contact_sheet src:3ab6797d -->
<!-- panel:contact_sheet src:ddc8d6ef -->
### Kontaktbogen

Auf dem Tab **Neben dem Export**. Ein Kontaktbogen zeigt die ganze Rolle als Filmstreifen auf einem Blatt Papier, wie ein Kontaktabzug in der Dunkelkammer.
Expand All @@ -514,7 +514,7 @@ Auf dem Tab **Neben dem Export**. Ein Kontaktbogen zeigt die ganze Rolle als Fil
* **Kontaktbogen…**: öffnet den Bogen. Ziehe an einer Kante des Papiers, um die Größe zu ändern.
* **Filmformat**: 35mm, 35mm Half Frame oder 120 (bei 120 mit **Bildgröße**).
* **Papier**, **Breite**, **Höhe** und **DPI**: Größe und Auflösung des Bogens. Das Standardpapier fasst eine Rolle mit 36 Aufnahmen.
* **Reihenfolge**: nach Dateiname, nach Datum oder nach Szene, mit einem neuen Streifen oder einer neuen Zeile für jede Szene.
* **Reihenfolge**: nach Dateiname, nach Datum, nach Rolle und Bildnummer oder nach Szene, mit einem neuen Streifen oder einer neuen Zeile für jede Szene.
* **Abzug**: **Wie bearbeitet** oder **Reiner Probeabzug**: die ganze Rolle mit einer Belichtung und normalem Kontrast. So siehst du, welche Negative dünn oder dicht sind.
* **Bilder wählen**: Klicke auf ein Bild, um es auszulassen oder wieder aufzunehmen. Verworfene Bilder sind zu Beginn ausgelassen.
* **Randaufdruck**: die Markierungen des Filmherstellers am Rand, zum Beispiel Filmname, Bildnummern und die farbigen Linien, die ein Film neben seinen Perforationen druckt.
Expand Down
6 changes: 3 additions & 3 deletions negpy/translations/USER_GUIDE.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,9 +132,9 @@ The **Film Strip** under the canvas shows the open frames in one row. Drag its t

The toolbar's **Roll** row holds:

* The roll's name and its count of frames, keepers and rejects.
* The roll's name and count of frames, keepers and rejects.
* The **filter box**: type part of a file name, or terms such as `film:portra iso:>=400` (below). The search button, or **Enter**, searches the whole library.
* **Sort**: by name, date or scene.
* **Sort**: name, date, scene or frame number.
* **Sheet filter**: all frames, keepers only, hide rejected, or unmarked only. The button is highlighted while a filter hides frames.
* **Roll Settings** (the tag): gear and film details for this frame, the selection or the roll.
* **Apply**: copies chosen settings of this frame to the selection or the roll.
Expand Down Expand Up @@ -711,7 +711,7 @@ On the **Alongside Exports** tab. A contact sheet shows the whole roll as film s
* **Contact Sheet…**: opens the sheet. Drag an edge of the paper to resize it.
* **Film format**: 35mm, 35mm Half Frame or 120 (with **Frame size** for 120).
* **Paper**, **Width**, **Height** and **DPI**: the sheet's size and resolution. The default paper holds a 36-exposure roll.
* **Order**: by file name, by date, or by scene with a new strip or row for each scene.
* **Order**: by file name, by date, by roll and frame number, or by scene with a new strip or row for each scene.
* **Print**: **As Edited**, or **Straight Proof**: the whole roll at one exposure and normal contrast, so you see which negatives are thin or dense.
* **Pick Frames**: click a frame to leave it out or put it back. Rejects start out.
* **Edge Print**: the film maker's markings along the edges, such as the film name, frame numbers and the colored lines a stock prints by its perforations.
Expand Down
8 changes: 4 additions & 4 deletions negpy/translations/USER_GUIDE.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室

ロールを読み込み、フィルムモードとスキャン装置を選びます。光源・レンズ補正、トリミング、ロール解析を行ってから、各フレームの濃度、コントラスト、色補正、修復を調整し、最後に書き出します。カード見出しのフレーム/ロールで適用範囲を選び、ロール設定への復帰で共有値を戻せます。個々のトリミング位置と回転はフレームに保持されます。

<!-- panel:frames src:e18d0aea -->
<!-- panel:frames src:c1f54a29 -->
## フィルムストリップ

キャンバスの下の**フィルムストリップ**は、開いているフレームを1列に表示します。上端をドラッグすると、フレームが大きくなります。
Expand All @@ -15,7 +15,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室

- ロール名と、フレーム数、キーパー数、不採用数。
- **絞り込みボックス**:ファイル名の一部、または `film:portra iso:>=400` のような条件(下記)を入力します。検索ボタンまたは **Enter** でライブラリ全体を検索します。
- **並べ替え**:名前、日付、シーン。
- **並べ替え**:名前、日付、シーン、またはロールとフレーム番号。
- **シートフィルター**:すべてのフレーム、キーパーのみ、拒否されたものを非表示、マークなしのみ。フィルターでフレームが隠れている間、ボタンが強調表示されます。
- **ロール設定**(タグのアイコン):このフレーム、選択、またはロールの機材とフィルムの情報。
- **適用**:このフレームの選んだ設定を、選択またはロールにコピーします。
Expand Down Expand Up @@ -509,7 +509,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室
- **自動濃度/グレード**:**書き出し時に確定**は、書き出した各フレームの自動の明るさとコントラストを固定し、後で変わらないようにします。**ロールに確定**は表示中のすべてのフレームで今すぐ固定します。
- **編集サイドカー**:**書き出し時に保存**は各元ファイルの横に編集を含む `.negpy` ファイルを書きます。**サイドカーのエクスポート**は表示中のすべてのフレームで書きます。フレームを右クリック →**サイドカーから編集をロード…**で読み戻します。

<!-- panel:contact_sheet src:3ab6797d -->
<!-- panel:contact_sheet src:ddc8d6ef -->
## コンタクトシート

**書き出しと一緒に**タブにあります。コンタクトシートは、暗室のコンタクトプリントのように、ロール全体をフィルムの帯として 1 枚の紙に並べます。
Expand All @@ -518,7 +518,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室
- **コンタクトシート…**:シートを開きます。紙の端をドラッグしてサイズを変えます。
- **フィルムフォーマット**:35mm、35mm ハーフサイズ、120(120 は**フレームサイズ**も設定)。
- **印画紙**、**幅**、**高さ**、**DPI**:シートのサイズと解像度。既定の紙で 36 枚撮りのロールが収まります。
- **順序**:ファイル名順、日付順、またはシーンごとに新しい帯か行を始めるシーン順。
- **順序**:ファイル名順、日付順、ロールとフレーム番号順、またはシーンごとに新しい帯か行を始めるシーン順。
- **プリント**:**編集済み**、または**ストレートプルーフ**:ロール全体を一つの露光と標準のコントラストでプリントし、どのネガが薄いか濃いかを見られます。
- **フレームの選択**:フレームをクリックして外したり戻したりします。不採用のフレームは最初から外れています。
- **エッジプリント**:フィルム名やフレーム番号、パーフォレーションの片側にある色付きラインなど、フィルムの縁のメーカーの印字。
Expand Down
Loading
Loading