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
4 changes: 2 additions & 2 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,11 @@ 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 date, or by scene with a new strip or row for each scene.
* **Order**: by file name, by date, 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 and frame numbers.
* **Roll Label**: the roll name, film, camera and date above the strips.
* **Roll Label**: the roll name, film, camera and date above the strips. Type your own text in **Label text** to print it instead.
* **White Paper**: prints on white, which saves ink on a home printer.
* **Film Edges**: off prints the frames upright, each at its own shape, in rows as on the Light Table. The sheet then has no perforations and no **Edge Print**.

Expand Down
4 changes: 2 additions & 2 deletions negpy/desktop/controller.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@
from negpy.services.export.contact_sheet_roll import (
FrameFacts,
SheetFrame,
creation_order,
name_order,
infer_format,
roll_label_text,
sheet_look,
Expand Down Expand Up @@ -8559,7 +8559,7 @@ def _on_contact_sheet_prepared(self, generation: int, facts: list) -> None:
return
self._contact_sheet_pending = None
self.set_status("", 0)
frames = creation_order(
frames = name_order(
[SheetFrame(asset, config, fact or FrameFacts()) for asset, config, fact in zip(pending["assets"], pending["configs"], facts)]
)
try:
Expand Down
95 changes: 59 additions & 36 deletions negpy/desktop/view/widgets/contact_sheet_dialog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np
from PyQt6.QtCore import QEvent, QPointF, QRectF, Qt, QTimer, pyqtSignal
from PyQt6.QtGui import QColor, QImage, QPainter, QPen
from PyQt6.QtWidgets import QDialog, QHBoxLayout, QPushButton, QSizePolicy, QVBoxLayout, QWidget
from PyQt6.QtWidgets import QDialog, QHBoxLayout, QLineEdit, QPushButton, QSizePolicy, QVBoxLayout, QWidget

from negpy.desktop.view.canvas.overlay import draw_resize_handles, draw_view_badge, hit_resize_handle, resize_cursor, view_badge_width
from negpy.desktop.view.styles.templates import (
Expand All @@ -18,6 +18,7 @@
set_hint_kind,
tool_toggle,
with_alpha,
wrap_tooltip,
)
from negpy.desktop.view.styles.theme import THEME
from negpy.desktop.view.widgets.dialog_geometry import remember_dialog_geometry
Expand All @@ -33,6 +34,7 @@
ILFORD_PAPERS,
PAPER_MAX,
PAPER_MIN,
SHEET_ORDERS,
ContactSheetSettings,
SheetPlan,
best_dpi,
Expand All @@ -50,6 +52,7 @@
SheetFrame,
SheetLook,
StraightProof,
creation_order,
print_aspect,
scene_breaks,
scene_of,
Expand All @@ -63,8 +66,8 @@
_CUSTOM_PAPER = len(ILFORD_PAPERS)
_PRINTS = (tr("As Edited"), tr("Straight Proof"))
_AS_EDITED, _STRAIGHT_PROOF = 0, 1
_ORDERS = (mark("Date"), mark("Scene"))
_BY_DATE, _BY_SCENE = 0, 1
_ORDERS = (mark("Name"), mark("Date"), mark("Scene"))
_BY_NAME, _BY_DATE, _BY_SCENE = 0, 1, 2
_EDITED_TILES, _PROOF_TILES, _SCENE_PROOF_TILES = 0, 1, 2
_CANVAS_PAD = 28.0
_TILE_REPAINT_MS = 120
Expand Down Expand Up @@ -308,8 +311,10 @@ def __init__(
self._tile_shapes = self._shape_sets[_EDITED_TILES]
self._turns = [0] * len(self._frames)
self._has_scenes = any(scene_of(frame) for frame in self._frames)
self._by_scene = settings.by_scene and self._has_scenes
self._display = scene_order(self._frames) if self._by_scene else list(range(len(self._frames)))
order = SHEET_ORDERS.index(settings.order)
self._order = _BY_NAME if order == _BY_SCENE and not self._has_scenes else order
self._by_scene = self._order == _BY_SCENE
self._display = self._order_indices(self._order)
self._page = 0
self._plan: Optional[SheetPlan] = None
self._look = look_for(film_format)
Expand Down Expand Up @@ -410,12 +415,12 @@ def _build_controls(self, settings: ContactSheetSettings) -> QWidget:

self.order_btn = StepCombo.of(
[tr(order) for order in _ORDERS],
tr("Lay the frames out in the order they were taken, or start a new strip for each scene."),
tr("Lay the frames out by file name, in the order they were taken, or with a new strip for each scene."),
)
self.order_btn.set_item_enabled(_BY_SCENE, self._has_scenes)
if not self._has_scenes:
self.order_btn.set_item_tooltip(_BY_SCENE, tr("No frame of this roll is in a scene"))
self.order_btn.setCurrentIndex(_BY_SCENE if self._by_scene else _BY_DATE)
self.order_btn.setCurrentIndex(self._order)
self.order_btn.currentIndexChanged.connect(self._on_order_changed)
col.addLayout(field_row(tr("Order"), self.order_btn))

Expand All @@ -437,44 +442,50 @@ def _build_controls(self, settings: ContactSheetSettings) -> QWidget:
self.pick_btn.toggled.connect(lambda _checked: self._schedule_render())
col.addWidget(self.pick_btn)

toggles = QHBoxLayout()
toggles.setSpacing(THEME.space_md)
self.film_edges_btn = labeled_toggle(
"fa5s.film",
tr("Film Edges"),
settings.film_edges,
tr(
"Print the frames on film strips with their perforations and edge print; off lays them upright in rows, as on the Light Table"
),
)
self.film_edges_btn.toggled.connect(self._on_film_edges_toggled)
self.edge_btn = labeled_toggle(
"fa5s.barcode",
tr("Edge Print"),
settings.edge_print,
tr("Print the maker's edge markings: stock name, frame numbers and the DX barcode"),
)
self.edge_btn.setEnabled(settings.film_edges)
self.edge_btn.toggled.connect(lambda _checked: self._restyle())
toggles.addWidget(self.edge_btn, 1)
self.label_btn = labeled_toggle(
"fa5s.tag", tr("Roll Label"), settings.roll_label, tr("Print the roll name, film, developer, camera and date above the strips")
)
self.label_btn.toggled.connect(self._on_label_toggled)
toggles.addWidget(self.label_btn, 1)
col.addLayout(toggles)
self.white_btn = labeled_toggle(
"fa5s.print",
tr("White Paper"),
settings.white_paper,
tr("Print on white paper with dark labels, to save ink on a home printer"),
)
self.white_btn.toggled.connect(lambda _checked: self._schedule_render())
self.film_edges_btn = labeled_toggle(
"fa5s.film",
tr("Film Edges"),
settings.film_edges,
tr(
"Print the frames on film strips with their perforations and edge print; off lays them upright in rows, as on the Light Table"
),
self.label_btn = labeled_toggle(
"fa5s.tag", tr("Roll Label"), settings.roll_label, tr("Print the roll name, film, developer, camera and date above the strips")
)
self.edge_btn.setEnabled(settings.film_edges)
self.film_edges_btn.toggled.connect(self._on_film_edges_toggled)
second = QHBoxLayout()
second.setSpacing(THEME.space_md)
second.addWidget(self.white_btn, 1)
second.addWidget(self.film_edges_btn, 1)
col.addLayout(second)
self.label_btn.toggled.connect(self._on_label_toggled)
for pair in ((self.film_edges_btn, self.edge_btn), (self.white_btn, self.label_btn)):
row = QHBoxLayout()
row.setSpacing(THEME.space_md)
for button in pair:
row.addWidget(button, 1)
col.addLayout(row)
self.label_edit = QLineEdit()
self.label_edit.setPlaceholderText(self._label_text)
self.label_edit.setToolTip(
wrap_tooltip(tr("Your own text for the roll label, or the roll name, film, camera and date when this is empty."))
)
self.label_edit.textChanged.connect(lambda _text: self._schedule_render())
# Return commits the text instead of pressing Export.
self.label_edit.installEventFilter(self)
self.label_edit.setEnabled(settings.roll_label)
col.addLayout(field_row(tr("Label text"), self.label_edit))

self.summary_label = hint_label()
self.summary_label.setWordWrap(True)
Expand Down Expand Up @@ -502,8 +513,8 @@ def _build_controls(self, settings: ContactSheetSettings) -> QWidget:
combos = (self.format_btn, self.frame_size_btn, self.paper_btn, self.dpi_btn, self.order_btn, self.print_btn)
# One width, the widest choice's, so no choice elides and the column keeps one right edge.
width = max(combo.minimumSizeHint().width() for combo in combos)
for combo in combos:
combo.setMinimumWidth(width)
for field in (*combos, self.label_edit):
field.setMinimumWidth(width)
return panel

def _geometry(self):
Expand Down Expand Up @@ -621,7 +632,8 @@ def _film_changed(self) -> None:
def _on_dpi_changed(self, index: int) -> None:
self._dpi = DPI_CHOICES[index]

def _on_label_toggled(self, _checked: bool) -> None:
def _on_label_toggled(self, checked: bool) -> None:
self.label_edit.setEnabled(checked)
self._set_paper(*self._paper, snap=False)

def _set_page(self, page: int) -> None:
Expand All @@ -634,9 +646,17 @@ def _request_tiles(self, variant: int) -> None:
return
self._generations[self._source.render(frames)] = variant

def _order_indices(self, order: int) -> list[int]:
if order == _BY_SCENE:
return scene_order(self._frames)
if order == _BY_DATE:
return creation_order(self._frames)
return list(range(len(self._frames)))

def _on_order_changed(self, index: int) -> None:
self._order = index
self._by_scene = index == _BY_SCENE
self._display = scene_order(self._frames) if self._by_scene else list(range(len(self._frames)))
self._display = self._order_indices(index)
self._apply_variant()

def _apply_variant(self) -> None:
Expand Down Expand Up @@ -773,6 +793,9 @@ def resizeEvent(self, event) -> None: # noqa: N802

def eventFilter(self, obj, event) -> bool: # noqa: N802
if event.type() == QEvent.Type.KeyPress and event.key() in (Qt.Key.Key_Return, Qt.Key.Key_Enter):
if obj is self.label_edit:
obj.clearFocus()
return True
if obj in (self.width_slider.spin, self.height_slider.spin):
obj.interpretText()
obj.clearFocus()
Expand Down Expand Up @@ -806,7 +829,7 @@ def settings(self) -> ContactSheetSettings:
self._dpi,
self._label_on(),
self.edge_btn.isChecked(),
self.order_btn.currentIndex() == _BY_SCENE,
SHEET_ORDERS[self._order],
self.white_btn.isChecked(),
self.film_edges_btn.isChecked(),
)
Expand Down Expand Up @@ -838,6 +861,6 @@ def look(self) -> SheetLook:
return replace(
self._look,
edge=edge,
label=self._label_text if self._label_on() else "",
label=(self.label_edit.text().strip() or self._label_text) if self._label_on() else "",
white_paper=self.white_btn.isChecked(),
)
9 changes: 6 additions & 3 deletions negpy/services/export/contact_sheet_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,9 @@ def frame_turns(upright_aspect: Optional[float], geometry: FilmGeometry, rotatio
return 3


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


@dataclass(frozen=True)
class ContactSheetSettings:
"""The film format is not stored: it is read from the frames each time."""
Expand All @@ -337,7 +340,7 @@ class ContactSheetSettings:
dpi: int = DEFAULT_DPI
roll_label: bool = True
edge_print: bool = True
by_scene: bool = False
order: str = "name" # one of SHEET_ORDERS
white_paper: bool = False
film_edges: bool = True # off prints the frames upright in rows, as the Light Table shows them

Expand All @@ -364,7 +367,7 @@ def flag(key: str, fallback: bool) -> bool:
dpi=dpi if dpi in DPI_CHOICES and not isinstance(dpi, bool) else default.dpi,
roll_label=flag("roll_label", default.roll_label),
edge_print=flag("edge_print", default.edge_print),
by_scene=flag("by_scene", default.by_scene),
order=data.get("order") if data.get("order") in SHEET_ORDERS else default.order,
white_paper=flag("white_paper", default.white_paper),
film_edges=flag("film_edges", default.film_edges),
)
Expand All @@ -376,7 +379,7 @@ def to_dict(self) -> dict[str, Any]:
"dpi": self.dpi,
"roll_label": self.roll_label,
"edge_print": self.edge_print,
"by_scene": self.by_scene,
"order": self.order,
"white_paper": self.white_paper,
"film_edges": self.film_edges,
}
13 changes: 9 additions & 4 deletions negpy/services/export/contact_sheet_roll.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,15 +188,20 @@ def read_frame_facts(asset: dict) -> FrameFacts:
)


def creation_order(frames: Sequence[SheetFrame]) -> list[SheetFrame]:
"""EXIF capture time when every frame has one, else file creation time: mixing them interleaves two clocks."""
def name_order(frames: Sequence[SheetFrame]) -> list[SheetFrame]:
return sorted(frames, key=lambda frame: (frame.name.lower(), frame.half))


def creation_order(frames: Sequence[SheetFrame]) -> list[int]:
"""Indices by EXIF capture time when every frame has one, else by file creation time: mixing them interleaves two clocks."""
by_exif = bool(frames) and all(f.facts.capture_time for f in frames)

def key(frame: SheetFrame) -> tuple[Any, ...]:
def key(index: int) -> tuple[Any, ...]:
frame = frames[index]
when: Any = frame.facts.capture_time if by_exif else frame.facts.birth_time
return (when, frame.name.lower(), frame.half)

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


def _crop_fractions(config: WorkspaceConfig) -> tuple[float, float]:
Expand Down
4 changes: 2 additions & 2 deletions negpy/translations/USER_GUIDE.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -700,11 +700,11 @@ 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 date, or by scene with a new strip or row for each scene.
* **Order**: by file name, by date, 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 and frame numbers.
* **Roll Label**: the roll name, film, camera and date above the strips.
* **Roll Label**: the roll name, film, camera and date above the strips. Type your own text in **Label text** to print it instead.
* **White Paper**: prints on white, which saves ink on a home printer.
* **Film Edges**: off prints the frames upright, each at its own shape, in rows as on the Light Table. The sheet then has no perforations and no **Edge Print**.

Expand Down
6 changes: 3 additions & 3 deletions negpy/translations/USER_GUIDE.ja.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ NegPy は、フィルムスキャンをポジ画像に仕上げる仮想暗室
- **自動濃度/グレード**:**書き出し時に確定**は、書き出した各フレームの自動の明るさとコントラストを固定し、後で変わらないようにします。**ロールに確定**は表示中のすべてのフレームで今すぐ固定します。
- **編集サイドカー**:**書き出し時に保存**は各元ファイルの横に編集を含む `.negpy` ファイルを書きます。**サイドカーのエクスポート**は表示中のすべてのフレームで書きます。フレームを右クリック →**サイドカーから編集をロード…**で読み戻します。

<!-- panel:contact_sheet src:a4bb042b -->
<!-- panel:contact_sheet src:e27d234d -->
## コンタクトシート

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

Expand Down
Loading
Loading