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
10 changes: 5 additions & 5 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Two cards: **Crop** sets the part of the picture you keep, and **Straighten** tu
Choose the part of the picture to keep, by hand or with Auto Crop.

* **Draw**: drag a crop on the picture. Drag a corner to resize it, the inside to move it, or outside the box to rotate. `Enter` or **Done** keeps the crop, and `Esc` puts back the crop you had before. The **×** removes a crop you drew. Drawing turns Auto Crop off.
* **Auto Crop**: finds the edges of the picture and crops to them.
* **Auto Crop** (also on the Geometry tab's bar): finds the edges of the picture and crops to them.
* **Ratio**: the crop shape, such as `3:2` or `1:1`, turned to fit the frame. **Free** uses the shape NegPy finds, and you can drag one side at a time. **Detect** picks the nearest standard shape.
* **Guide**: composition lines, such as thirds, while you draw. The rotate button turns them.
* **Mode** (Auto Crop Settings fold): **Image only** crops to the picture. **Film edge** keeps the whole film with its border and holes.
Expand All @@ -240,7 +240,7 @@ Turn the picture and make it level.

* **Rotate** and **Flip**: turn the picture a quarter turn, or mirror it. The toolbar has the same buttons.
* **Level**: draw a line along the horizon or a vertical edge, and the picture turns to make it level.
* **Square to Film**: lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted.
* **Square to Film** (also on the Geometry tab's bar): lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted.
* **Fine Rotation**: turns the picture by small amounts. Positive is clockwise.
* **Tilt** and **Swing** (fold): straighten a building shot from below (Tilt) or a wall shot from one side (Swing). **Draw Lines** sets both from lines along the four film edges.
* **Crop Empty Corners**: crops away the empty corners that rotation, Tilt and Swing leave.
Expand Down Expand Up @@ -271,7 +271,7 @@ How light or dark the print is, and how much contrast it has.

* **Print Density**: how light or dark the print is (the darkroom's exposure time). Lower is lighter.
* **ISO-R Grade**: contrast (the darkroom's paper grade). A lower number gives more contrast; about 110 is normal.
* **Auto**: **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has:
* **Auto** (the wand, also on the Exposure tab's bar): **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has:
* **Set Targets…**: the brightness and contrast Auto aims for.
* **Bake Auto Density/Grade**: keeps this frame's automatic values and turns Auto off for it.
* **Hold Black**: when you lighten the shadows, the deepest blacks stay black.
Expand Down Expand Up @@ -397,8 +397,8 @@ On a lith print only Selenium and Gold work. On a cyanotype no toner works.
Removes dust, hairs and scratches. Each repair is rebuilt from the clean film around it.

* **Overlay**: **Marked** shows what dust removal found, **IR** the scan's infrared channel.
* **Optical Removal**: finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu.
* **IR Removal** (scans with an infrared channel): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan.
* **Optical Removal** (also on the Finish tab's bar): finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu.
* **IR Removal** (scans with an infrared channel, also on the Finish tab's bar): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan.
* **Heal**: click or paint over dust. Only marks that stand out are repaired, so paint generously.
* **Scratch**: click points along a scratch or hair, then press Enter.
* **Line**: click once on a long straight scratch, and NegPy finds and repairs the whole line. **Line Sensitivity**: lower finds fainter lines.
Expand Down
23 changes: 23 additions & 0 deletions negpy/desktop/view/sidebar/controls_panel.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from negpy.kernel.system.i18n import tr
import qtawesome as qta
from PyQt6.QtWidgets import (
QMenu,
QWidget,
Expand Down Expand Up @@ -422,6 +423,16 @@ def _init_ui(self) -> None:
page_layout.addWidget(header)
if key == "tone":
self._add_pick_better(header)
self._add_auto_mirror(header)
elif key == "geometry":
self.auto_crop_mirror_btn = header.add_mirror(self.autocrop_sidebar.auto_frame_btn)
self.square_to_film_mirror_btn = header.add_mirror(
self.geometry_sidebar.auto_skew_btn,
qta.icon("fa5s.ruler-combined", color=THEME.text_hint, color_disabled=THEME.text_muted),
)
elif key == "finish":
self.optical_removal_mirror_btn = header.add_mirror(self.retouch_sidebar.auto_dust_btn)
self.ir_removal_mirror_btn = header.add_mirror(self.retouch_sidebar.ir_dust_btn)
for section in sections:
page_layout.addWidget(section)
page_layout.addStretch(1)
Expand Down Expand Up @@ -453,6 +464,15 @@ def _add_pick_better(self, header: TabHeader) -> None:
# Session state, not config, so the button follows the controller rather than sync_ui.
self.controller.pick_better_changed.connect(self._sync_pick_better_btn)

def _add_auto_mirror(self, header: TabHeader) -> None:
"""The Tone card's Auto button again on the tab's header: the one menu, so both stay in step."""
auto = self.tone_sidebar.auto_btn
icon = qta.icon("fa5s.magic", color=THEME.text_hint, color_on=THEME.text_primary, color_disabled=THEME.text_muted)
self.auto_mirror_btn = header.add_lead_toggle(icon, auto.plain_tooltip)
self.auto_mirror_btn.setMenu(auto.choice_menu)
self.auto_mirror_btn.setChecked(auto.isChecked())
auto.toggled.connect(self.auto_mirror_btn.setChecked)

@staticmethod
def _pick_better_tooltip(printing: bool = False) -> str:
if printing:
Expand Down Expand Up @@ -566,6 +586,9 @@ def apply_shortcut_tooltips(self) -> None:
(self.retouch_sidebar.auto_dust_btn, "toggle_optical_removal"),
(self.retouch_sidebar.right_click_btn, "toggle_right_click_excludes"),
(self.retouch_sidebar.ir_dust_btn, "toggle_ir_removal"),
(self.optical_removal_mirror_btn, "toggle_optical_removal"),
(self.ir_removal_mirror_btn, "toggle_ir_removal"),
(self.auto_crop_mirror_btn, "auto_crop"),
(self.flatfield_sidebar.enable_btn, "toggle_flat_field"),
(self.flatfield_sidebar.check_btn, "toggle_flatfield_peek"),
(self.autocrop_sidebar.auto_frame_btn, "auto_crop"),
Expand Down
2 changes: 1 addition & 1 deletion negpy/desktop/view/sidebar/retouch.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ def _init_ui(self) -> None:
self.optical_rail = SliderGroup(self.threshold_slider, self.hair_threshold_slider, self.auto_size_slider)
self.layout.addWidget(self.optical_rail)

self.ir_dust_btn = self._small_toggle("fa5s.broom", tr("IR Removal"), conf.ir_dust_remove, _IR_REMOVAL_TIP, align_left=True)
self.ir_dust_btn = self._small_toggle("mdi6.heat-wave", tr("IR Removal"), conf.ir_dust_remove, _IR_REMOVAL_TIP, align_left=True)
self.layout.addWidget(self.ir_dust_btn)
self.ir_threshold_slider = CompactSlider(tr("IR Threshold"), 0.05, 0.95, float(conf.ir_threshold))
self.ir_rail = SliderGroup(self.ir_threshold_slider)
Expand Down
4 changes: 2 additions & 2 deletions negpy/desktop/view/sidebar/scan.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,11 +332,11 @@ def _init_ui(self) -> None:
self.samples_widget = self._row_widget(field_row(tr("Samples"), self.samples_btn))
quality.addWidget(self.samples_widget)

self.ir_btn = tool_toggle("fa5s.layer-group", tr("IR"), tr("Scan a separate infrared channel for dust detection"))
self.ir_btn = tool_toggle("mdi6.heat-wave", tr("IR"), tr("Scan a separate infrared channel for dust detection"))
# IR and ICE read the same pass, and ICE bakes its repair into the file, so a raw IR
# plane beside it would only have cleaned pixels to detect dust in: one choice of three.
self.dust_btn = SegmentedChoice(
(("", tr("Off")), ("fa5s.layer-group", tr("IR")), ("fa5s.broom", tr("ICE"))),
(("", tr("Off")), ("mdi6.heat-wave", tr("IR")), ("fa5s.broom", tr("ICE"))),
tr("How the scan uses the infrared pass against dust: not at all, as a separate channel, or as ICE in the scanner."),
data=("off", "ir", "ice"),
)
Expand Down
39 changes: 37 additions & 2 deletions negpy/desktop/view/widgets/tab_header.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
from typing import Iterable

import qtawesome as qta
from PyQt6.QtCore import pyqtSignal
from PyQt6.QtCore import QEvent, QObject, pyqtSignal
from PyQt6.QtGui import QIcon
from PyQt6.QtWidgets import QPushButton
from PyQt6.QtWidgets import QAbstractButton, QPushButton

from negpy.desktop.view.confirm import confirm_reset_tab
from negpy.desktop.view.styles.templates import header_button, wrap_tooltip
Expand Down Expand Up @@ -57,6 +57,18 @@ def add_lead_toggle(self, icon: QIcon, tooltip: str) -> QPushButton:
self.icon_label.hide()
return btn

def add_mirror(self, source: QAbstractButton, icon: QIcon | None = None) -> QPushButton:
"""A lead button that clicks `source`, a card's own button, and follows its check,
its enabled state and whether the card hides it. `icon` replaces an icon drawn for a
labeled button, which is always lit."""
btn = self.add_lead_toggle(icon or source.icon(), source.toolTip())
btn.plain_tooltip = getattr(source, "plain_tooltip", "")
btn.setCheckable(source.isCheckable())
btn.clicked.connect(lambda: (source.click(), btn.setChecked(source.isChecked())))
source.toggled.connect(btn.setChecked)
_MirrorSync(source, btn)
return btn

def bind(self, sections: Iterable[CollapsibleSection]) -> None:
self._sections = tuple(sections)
self.refresh()
Expand Down Expand Up @@ -96,3 +108,26 @@ def _sync_cards_button(self) -> None:
collapse = self._any_expanded()
self.cards_btn.setIcon(qta.icon("fa5s.angle-double-up" if collapse else "fa5s.angle-double-down", color=THEME.text_hint))
self.cards_btn.setToolTip(wrap_tooltip(tr("Collapse every card on this tab") if collapse else tr("Expand every card on this tab")))


class _MirrorSync(QObject):
"""Copies a source button's explicit hide and enabled state to its mirror. ShowToParent and
HideToParent arrive even while a collapsed card keeps the source off screen."""

_EVENTS = (QEvent.Type.ShowToParent, QEvent.Type.HideToParent, QEvent.Type.EnabledChange)

def __init__(self, source: QAbstractButton, mirror: QPushButton):
super().__init__(mirror)
self._source, self._mirror = source, mirror
source.installEventFilter(self)
self._sync()

def eventFilter(self, obj, event) -> bool: # noqa: N802
if event.type() in self._EVENTS:
self._sync()
return False

def _sync(self) -> None:
self._mirror.setVisible(not self._source.isHidden())
self._mirror.setEnabled(self._source.isEnabled())
self._mirror.setChecked(self._source.isChecked())
10 changes: 5 additions & 5 deletions negpy/translations/USER_GUIDE.en.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ Two cards: **Crop** sets the part of the picture you keep, and **Straighten** tu
Choose the part of the picture to keep, by hand or with Auto Crop.

* **Draw**: drag a crop on the picture. Drag a corner to resize it, the inside to move it, or outside the box to rotate. `Enter` or **Done** keeps the crop, and `Esc` puts back the crop you had before. The **×** removes a crop you drew. Drawing turns Auto Crop off.
* **Auto Crop**: finds the edges of the picture and crops to them.
* **Auto Crop** (also on the Geometry tab's bar): finds the edges of the picture and crops to them.
* **Ratio**: the crop shape, such as `3:2` or `1:1`, turned to fit the frame. **Free** uses the shape NegPy finds, and you can drag one side at a time. **Detect** picks the nearest standard shape.
* **Guide**: composition lines, such as thirds, while you draw. The rotate button turns them.
* **Mode** (Auto Crop Settings fold): **Image only** crops to the picture. **Film edge** keeps the whole film with its border and holes.
Expand All @@ -240,7 +240,7 @@ Turn the picture and make it level.

* **Rotate** and **Flip**: turn the picture a quarter turn, or mirror it. The toolbar has the same buttons.
* **Level**: draw a line along the horizon or a vertical edge, and the picture turns to make it level.
* **Square to Film**: lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted.
* **Square to Film** (also on the Geometry tab's bar): lines the picture up with the edges of the film. It ignores what is in the picture, so a photo taken at a tilt stays tilted.
* **Fine Rotation**: turns the picture by small amounts. Positive is clockwise.
* **Tilt** and **Swing** (fold): straighten a building shot from below (Tilt) or a wall shot from one side (Swing). **Draw Lines** sets both from lines along the four film edges.
* **Crop Empty Corners**: crops away the empty corners that rotation, Tilt and Swing leave.
Expand Down Expand Up @@ -271,7 +271,7 @@ How light or dark the print is, and how much contrast it has.

* **Print Density**: how light or dark the print is (the darkroom's exposure time). Lower is lighter.
* **ISO-R Grade**: contrast (the darkroom's paper grade). A lower number gives more contrast; about 110 is normal.
* **Auto**: **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has:
* **Auto** (the wand, also on the Exposure tab's bar): **Auto Density** and **Auto Grade** set brightness and contrast for each frame, so a roll prints evenly. A slider then shows the automatic value: move it to adjust, double-click it to go back. The Auto menu also has:
* **Set Targets…**: the brightness and contrast Auto aims for.
* **Bake Auto Density/Grade**: keeps this frame's automatic values and turns Auto off for it.
* **Hold Black**: when you lighten the shadows, the deepest blacks stay black.
Expand Down Expand Up @@ -397,8 +397,8 @@ On a lith print only Selenium and Gold work. On a cyanotype no toner works.
Removes dust, hairs and scratches. Each repair is rebuilt from the clean film around it.

* **Overlay**: **Marked** shows what dust removal found, **IR** the scan's infrared channel.
* **Optical Removal**: finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu.
* **IR Removal** (scans with an infrared channel): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan.
* **Optical Removal** (also on the Finish tab's bar): finds dust and hairs by itself. **Spot Threshold** and **Hair Threshold**: lower finds more but can remove real detail; 1 is off. **Size**: the largest dust to remove. To protect an area, right-drag over it or right-click a mark; the cursor button skips the right-click menu.
* **IR Removal** (scans with an infrared channel, also on the Finish tab's bar): finds dust in infrared, where the picture does not show. **IR Threshold**: lower finds more. B&W and Kodachrome are skipped: their picture shows in infrared too. The channel comes from 4-channel TIFF and DNG scans or an `_IR.tif` file beside the scan.
* **Heal**: click or paint over dust. Only marks that stand out are repaired, so paint generously.
* **Scratch**: click points along a scratch or hair, then press Enter.
* **Line**: click once on a long straight scratch, and NegPy finds and repairs the whole line. **Line Sensitivity**: lower finds fainter lines.
Expand Down
Loading
Loading