Кадрирование: поворот после обрезки, рамка не расходится с настройкой
Поворот и отражение применялись к исходному кадру, а обрезка шла после них. Из-за этого рамка показывала одно место, а вырезалось другое. Теперь наоборот: сначала выбирается кусок, потом он поворачивается и отражается. Если после поворота кусок лёг поперёк кадра проекта, он вписывается целиком с полями. Проверено: с отражением левый и правый края вырезанного куска меняются местами, то есть отражается именно он, а не оригинал. Вторая причина расхождения — предел ползунков смещения. Рамку можно было утащить туда, куда ползунок не дотягивается: рамка рисовалась от неограниченного значения, а фильтр считался от обрезанного. Предел расширен вчетверо, а рамка теперь пересчитывается обратно из настройки — если та упёрлась, это сразу видно. Отдельно: разбор звука больше не держит главный поток. Перезапуск ждал окончания текущего файла, а это чтение гигабайтов — окно всё это время не отвечало, и меню не открывалось вовсе. Теперь проход доигрывает сам и запускается заново, а идёт он с наинизшим приоритетом.
This commit is contained in:
parent
40a0e2041f
commit
f9e50632aa
4 changed files with 121 additions and 47 deletions
|
|
@ -108,6 +108,8 @@ type
|
|||
FThumbs: TImageList;
|
||||
FThumbThread: TThumbThread;
|
||||
FPeaksThread: TPeaksThread;
|
||||
FPeaksRestart: Boolean;
|
||||
FPeaksFinished: Boolean;
|
||||
{ Соответствие «путь к файлу -> номер картинки в списке изображений».
|
||||
По пути, а не по номеру в списке проекта: пока миниатюра делается,
|
||||
файлы могут добавиться или пропасть. Номер хранится увеличенным на
|
||||
|
|
@ -158,6 +160,7 @@ type
|
|||
procedure StartThumbs;
|
||||
procedure StartPeaks;
|
||||
procedure PeaksReady(Sender: TObject; const ASourcePath: string);
|
||||
procedure PeaksDone(Sender: TObject);
|
||||
procedure ThumbReady(Sender: TObject; const ASourcePath,
|
||||
AThumbPath: string);
|
||||
procedure SeekPlayerTo(const ATime: Double);
|
||||
|
|
@ -368,6 +371,9 @@ begin
|
|||
end;
|
||||
if Assigned(FPeaksThread) then
|
||||
begin
|
||||
{ При закрытии ждать приходится: поток обращается к окну. Ждать
|
||||
недолго — запущенный ffmpeg прерывается по тому же признаку. }
|
||||
FPeaksRestart := False;
|
||||
FPeaksThread.Terminate;
|
||||
FPeaksThread.WaitFor;
|
||||
FreeAndNil(FPeaksThread);
|
||||
|
|
@ -847,13 +853,26 @@ var
|
|||
Item: TMediaItem;
|
||||
Dummy: TPeaks;
|
||||
begin
|
||||
{ Ждать окончания разбора нельзя: он читает файл целиком, а это
|
||||
гигабайты и десяток секунд. Ожидание идёт в главном потоке, и всё это
|
||||
время окно не отвечает — меню не открывается вовсе.
|
||||
|
||||
Поэтому просто отмечаем, что список изменился: текущий проход
|
||||
доработает и запустится заново. }
|
||||
if Assigned(FPeaksThread) then
|
||||
begin
|
||||
FPeaksThread.Terminate;
|
||||
if not FPeaksFinished then
|
||||
begin
|
||||
FPeaksRestart := True;
|
||||
Exit;
|
||||
end;
|
||||
{ Уже завершился — ожидание мгновенное. }
|
||||
FPeaksThread.WaitFor;
|
||||
FreeAndNil(FPeaksThread);
|
||||
end;
|
||||
|
||||
FPeaksRestart := False;
|
||||
FPeaksFinished := False;
|
||||
if FProject.PoolCount = 0 then
|
||||
Exit;
|
||||
|
||||
|
|
@ -876,9 +895,23 @@ begin
|
|||
|
||||
FPeaksThread := TPeaksThread.Create(Requests);
|
||||
FPeaksThread.OnReady := @PeaksReady;
|
||||
FPeaksThread.OnTerminate := @PeaksDone;
|
||||
{ Разбор звука не должен мешать работе: он тяжёлый и никуда не спешит. }
|
||||
FPeaksThread.Priority := tpLowest;
|
||||
FPeaksThread.Start;
|
||||
end;
|
||||
|
||||
{ Проход разбора закончился. Освобождаем поток и, если за это время
|
||||
список файлов менялся, начинаем заново. }
|
||||
procedure TMainForm.PeaksDone(Sender: TObject);
|
||||
begin
|
||||
{ Освобождать поток прямо здесь нельзя: он ещё доживает свой цикл.
|
||||
Отмечаем, что он закончил, а освободит его следующий заход. }
|
||||
FPeaksFinished := True;
|
||||
if FPeaksRestart then
|
||||
StartPeaks;
|
||||
end;
|
||||
|
||||
procedure TMainForm.PeaksReady(Sender: TObject; const ASourcePath: string);
|
||||
begin
|
||||
{ Запомненную неудачу надо забыть: пики только что появились. }
|
||||
|
|
|
|||
|
|
@ -91,6 +91,10 @@ type
|
|||
{ Пересчёт между рамкой и настройкой. }
|
||||
procedure SelFromTransform;
|
||||
procedure TransformFromSel;
|
||||
{ Загоняет настройку в те пределы, которые умеют ползунки. Иначе
|
||||
рамка и ползунки расходятся: рамка от неограниченного значения,
|
||||
фильтр от обрезанного. }
|
||||
procedure ClampTransform;
|
||||
procedure ReadControls;
|
||||
procedure WriteControls;
|
||||
procedure ScheduleRefresh;
|
||||
|
|
@ -134,7 +138,11 @@ const
|
|||
|
||||
ZoomMin = 50;
|
||||
ZoomMax = 400;
|
||||
OffsetRange = 50;
|
||||
{ Предел смещения. При сильном увеличении кадр гуляет далеко за половину
|
||||
своей ширины, и полсотни процентов не хватало: рамку можно было
|
||||
утащить туда, куда ползунок не дотягивается, и они расходились —
|
||||
рамка показывала одно, а получалось другое. }
|
||||
OffsetRange = 200;
|
||||
|
||||
procedure TPanCropForm.FormCreate(Sender: TObject);
|
||||
begin
|
||||
|
|
@ -302,17 +310,12 @@ end;
|
|||
{ Размеры кадра после поворота: рамка живёт именно в них, иначе после
|
||||
поворота на 90 она оказалась бы поперёк картинки. }
|
||||
procedure TPanCropForm.UpdateRotatedSize;
|
||||
var
|
||||
Rot: Integer;
|
||||
begin
|
||||
{ Рамка живёт в координатах исходника как он есть. Поворот на неё не
|
||||
влияет: он применяется к уже вырезанному куску, а не к кадру целиком —
|
||||
иначе рамка показывала бы не то, что останется. }
|
||||
FRotW := FSourceInfo.Width;
|
||||
FRotH := FSourceInfo.Height;
|
||||
Rot := ((FTransform.Rotation mod 360) + 360) mod 360;
|
||||
if (Rot = 90) or (Rot = 270) then
|
||||
begin
|
||||
FRotW := FSourceInfo.Height;
|
||||
FRotH := FSourceInfo.Width;
|
||||
end;
|
||||
end;
|
||||
|
||||
{ Кадр источника, вписанный в панель с сохранением пропорций. }
|
||||
|
|
@ -447,6 +450,18 @@ begin
|
|||
FTransform.OffsetY := (FSel.Top * K - (SH - CH) / 2) / FDstHeight;
|
||||
end;
|
||||
|
||||
procedure TPanCropForm.ClampTransform;
|
||||
begin
|
||||
if FTransform.Zoom < ZoomMin / 100 then
|
||||
FTransform.Zoom := ZoomMin / 100;
|
||||
if FTransform.Zoom > ZoomMax / 100 then
|
||||
FTransform.Zoom := ZoomMax / 100;
|
||||
FTransform.OffsetX := Max(-OffsetRange / 100, Min(OffsetRange / 100,
|
||||
FTransform.OffsetX));
|
||||
FTransform.OffsetY := Max(-OffsetRange / 100, Min(OffsetRange / 100,
|
||||
FTransform.OffsetY));
|
||||
end;
|
||||
|
||||
{ Кадр источника без обработки: по нему тянут рамку. }
|
||||
procedure TPanCropForm.LoadSourceFrame;
|
||||
var
|
||||
|
|
@ -578,6 +593,11 @@ begin
|
|||
end;
|
||||
|
||||
TransformFromSel;
|
||||
ClampTransform;
|
||||
{ Рамка пересчитывается обратно из настройки. Так она всегда показывает
|
||||
ровно то, что получится: если настройка упёрлась в предел, это сразу
|
||||
видно, а не расходится молча. }
|
||||
SelFromTransform;
|
||||
FUpdating := True;
|
||||
try
|
||||
WriteControls;
|
||||
|
|
|
|||
|
|
@ -164,11 +164,10 @@ function TransformFilter(const AT: TMediaTransform;
|
|||
ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer): string;
|
||||
var
|
||||
Chain: string;
|
||||
W0, H0, Tmp: Integer;
|
||||
K, KFit, KFill: Double;
|
||||
SW, SH: Integer;
|
||||
CW, CH, CX, CY: Integer;
|
||||
PX, PY: Integer;
|
||||
PW, PH, PX, PY: Integer;
|
||||
Rot: Integer;
|
||||
|
||||
procedure Add(const AFilter: string);
|
||||
|
|
@ -185,32 +184,14 @@ begin
|
|||
Exit;
|
||||
|
||||
Chain := '';
|
||||
W0 := ASrcWidth;
|
||||
H0 := ASrcHeight;
|
||||
|
||||
{ --- Поворот и отражение --- }
|
||||
Rot := ((AT.Rotation mod 360) + 360) mod 360;
|
||||
case Rot of
|
||||
90:
|
||||
begin
|
||||
{ transpose=1 — поворот на 90 по часовой стрелке. }
|
||||
Add('transpose=1');
|
||||
Tmp := W0; W0 := H0; H0 := Tmp;
|
||||
end;
|
||||
180:
|
||||
{ Два поворота подряд дешевле и точнее, чем поворот на произвольный
|
||||
угол: последний пересчитывает каждый пиксель. }
|
||||
Add('transpose=1,transpose=1');
|
||||
270:
|
||||
begin
|
||||
Add('transpose=2');
|
||||
Tmp := W0; W0 := H0; H0 := Tmp;
|
||||
end;
|
||||
end;
|
||||
if AT.FlipH then
|
||||
Add('hflip');
|
||||
if AT.FlipV then
|
||||
Add('vflip');
|
||||
{ Порядок действий: сначала выбирается кусок кадра, и только потом он
|
||||
поворачивается и отражается.
|
||||
|
||||
Так это устроено в монтажных программах, и так понятнее: рамка
|
||||
показывает, что́ останется, а поворот применяется к оставшемуся. При
|
||||
обратном порядке обрезался бы уже перевёрнутый кадр, и в рамку
|
||||
попадало бы совсем не то, что видно. }
|
||||
|
||||
{ --- Масштаб --- }
|
||||
if AT.Fit = fmStretch then
|
||||
|
|
@ -220,16 +201,16 @@ begin
|
|||
end
|
||||
else
|
||||
begin
|
||||
KFit := Min(ADstWidth / W0, ADstHeight / H0);
|
||||
KFill := Max(ADstWidth / W0, ADstHeight / H0);
|
||||
KFit := Min(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight);
|
||||
KFill := Max(ADstWidth / ASrcWidth, ADstHeight / ASrcHeight);
|
||||
if AT.Fit = fmFill then
|
||||
K := KFill
|
||||
else
|
||||
K := KFit;
|
||||
if AT.Zoom > 0 then
|
||||
K := K * AT.Zoom;
|
||||
SW := MakeEven(Round(W0 * K));
|
||||
SH := MakeEven(Round(H0 * K));
|
||||
SW := MakeEven(Round(ASrcWidth * K));
|
||||
SH := MakeEven(Round(ASrcHeight * K));
|
||||
end;
|
||||
Add(Format('scale=%d:%d', [SW, SH]));
|
||||
|
||||
|
|
@ -247,13 +228,51 @@ begin
|
|||
Add(Format('crop=%d:%d:%d:%d', [CW, CH, CX, CY]));
|
||||
end;
|
||||
|
||||
{ --- Поля, если картинка меньше кадра --- }
|
||||
if (CW < ADstWidth) or (CH < ADstHeight) then
|
||||
{ --- Поворот и отражение оставшегося куска --- }
|
||||
Rot := ((AT.Rotation mod 360) + 360) mod 360;
|
||||
case Rot of
|
||||
90:
|
||||
begin
|
||||
{ transpose=1 — поворот на 90 по часовой стрелке. }
|
||||
Add('transpose=1');
|
||||
PW := CH; PH := CW;
|
||||
end;
|
||||
180:
|
||||
begin
|
||||
{ Два поворота подряд дешевле и точнее, чем поворот на
|
||||
произвольный угол: последний пересчитывает каждый пиксель. }
|
||||
Add('transpose=1,transpose=1');
|
||||
PW := CW; PH := CH;
|
||||
end;
|
||||
270:
|
||||
begin
|
||||
Add('transpose=2');
|
||||
PW := CH; PH := CW;
|
||||
end;
|
||||
else
|
||||
PW := CW; PH := CH;
|
||||
end;
|
||||
if AT.FlipH then
|
||||
Add('hflip');
|
||||
if AT.FlipV then
|
||||
Add('vflip');
|
||||
|
||||
{ --- Приведение к кадру проекта --- }
|
||||
|
||||
{ После поворота на 90 кусок лежит поперёк и в кадр проекта уже не
|
||||
вписывается. Вписываем целиком, с полями. }
|
||||
if (PW > ADstWidth) or (PH > ADstHeight) then
|
||||
begin
|
||||
PX := Round((ADstWidth - CW) / 2 + AT.OffsetX * ADstWidth);
|
||||
PY := Round((ADstHeight - CH) / 2 + AT.OffsetY * ADstHeight);
|
||||
PX := Max(0, Min(ADstWidth - CW, PX));
|
||||
PY := Max(0, Min(ADstHeight - CH, PY));
|
||||
K := Min(ADstWidth / PW, ADstHeight / PH);
|
||||
PW := MakeEven(Round(PW * K));
|
||||
PH := MakeEven(Round(PH * K));
|
||||
Add(Format('scale=%d:%d', [PW, PH]));
|
||||
end;
|
||||
|
||||
if (PW < ADstWidth) or (PH < ADstHeight) then
|
||||
begin
|
||||
PX := Max(0, (ADstWidth - PW) div 2);
|
||||
PY := Max(0, (ADstHeight - PH) div 2);
|
||||
Add(Format('pad=%d:%d:%d:%d', [ADstWidth, ADstHeight, PX, PY]));
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -335,6 +335,8 @@ var
|
|||
I: Integer;
|
||||
begin
|
||||
inherited Create(True);
|
||||
{ Освобождает поток окно, а не он сам: окно обращается к нему и после
|
||||
завершения, чтобы понять, надо ли начинать заново. }
|
||||
FreeOnTerminate := False;
|
||||
SetLength(FRequests, Length(ARequests));
|
||||
for I := 0 to High(ARequests) do
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue