From 235cd00e9afe4992097cccccf132943b29dba8ec Mon Sep 17 00:00:00 2001 From: 4aiman Date: Thu, 1 Oct 2026 00:04:57 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D1=80=D0=B5=D0=B4=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=81=D0=BC=D0=BE=D1=82=D1=80=20=D0=BF=D0=BE=D0=BA=D0=B0=D0=B7?= =?UTF-8?q?=D1=8B=D0=B2=D0=B0=D0=B5=D1=82=20=D0=BA=D0=B0=D0=B4=D1=80=D0=B8?= =?UTF-8?q?=D1=80=D0=BE=D0=B2=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=BA=D0=BB=D0=B8?= =?UTF-8?q?=D0=BF=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Клип со своим кадрированием играл в окне во весь кадр: цепочка фильтров у mpv одна на весь список кусков, а включить её на одном куске и выключить на следующем нечем — из фильтров ffmpeg по времени переключается только наложение. Обошлось без цепочки вовсе. У вывода mpv своя обрезка, и делает её видеокарта: курсор зашёл в клип с кадрированием — выставили, вышел — сняли. Ни перестройки цепочки, ни копирования кадров из памяти видеокарты в обычную. Рамку источника считает теперь один код на две стороны: по ней окно «Кадрирование и поворот» рисует рамку, и её же плеер отдаёт как обрезку. Проверка в tests/lfmcheck сверяет рамку с фильтром сборки: увеличение 2.77 даёт 970x546 посередине, и это ровно то, что вырезает crop=2688:1512:2379:1338 после scale=7446:4188. Проверено глазами на невидимом экране, на настоящем материале с дрона: картинка в окне совпала с готовым роликом, на границе с обычным клипом обрезка снимается и возвращается. Сверено со сборкой из прошлого коммита — там на том же месте кадр без кадрирования. Не покрыто и записано в BACKLOG: отражение (свойства для него нет), растяжение и участки с наложением — там кадр уже сведён цепочкой. --- BACKLOG.md | 49 ++++++++------------- README.md | 21 ++++++--- src/uFlatten.pas | 32 ++++++++++++++ src/uMainForm.pas | 86 +++++++++++++++++++++++++++++++++++++ src/uMpvPlayer.pas | 42 ++++++++++++++++++ src/uPanCropForm.pas | 39 ++--------------- src/uTransform.pas | 71 ++++++++++++++++++++++++++++++ tests/lfmcheck/lfmcheck.lpr | 59 +++++++++++++++++++++++++ 8 files changed, 329 insertions(+), 70 deletions(-) diff --git a/BACKLOG.md b/BACKLOG.md index 24a38ff..ec4b089 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -76,46 +76,35 @@ ffmpeg проблемой не является: он запускается о MP4/H.264 нельзя — контейнер и кодек альфу не хранят. Для этого нужен отдельный вывод в VP9 или ProRes 4444. -## Предпросмотр не показывает кадрирование обычного клипа +## Предпросмотр: чего он про кадрирование всё ещё не показывает -Кадрирование и поворот видны в окне только у клипов с ключом или -прозрачностью: их плеер кладёт поверх основы сам и применяет к ним -цепочку фильтров. Обычный клип идёт списком кусков, а список фильтровать -нечем — он играется как есть, во весь кадр. В готовом ролике при этом всё -верно: такой кусок помечен на пересборку и обкладывается полями. +Кадрирование обычного клипа окно теперь показывает — обрезкой на выводе +плеера, её делает видеокарта даром. Остались три случая, где оно +по-прежнему играется как есть, и для них точную картинку даёт окно +«Кадрирование и поворот»: -Проверено опытом: у клипа выключен только ключ, кадрирование оставлено — -окно показывает его во весь кадр. +**Отражение.** У вывода mpv нет свойства для отражения, только для +поворота. Понадобится — придётся вести такой клип через цепочку. -Почему нельзя просто добавить фильтр. Цепочка у mpv одна на весь список -кусков, а кадрирование задаётся каждому клипу своё. Включить фильтр на -одном куске и выключить на следующем нечем: из фильтров ffmpeg по времени -переключаются только те, у кого есть поддержка временной шкалы, а у -`crop` и `scale` её нет — есть только у `overlay`. Значит время включения -выражается единственным способом: раздвоить поток, одну ветку обрезать и -растянуть, и наложить её на другую с `enable='between(t,...)'`. Ветка при -этом работает на каждом кадре фильма, а не только там, где клип виден. +**Растяжение.** Кадр жмётся неравномерно, рамкой это не описать. -Замер (DJI 2688x1512, HEVC 55 Мбит/с, 600 кадров, `--vo=null`): +**Участок с наложением.** Там кадр уже сведён цепочкой, и обрезка на +выводе резала бы сведённое целиком, вместе с нижним слоем. Такой клип +нужно вести через ту же цепочку, что и само наложение. + +Цена пути через цепочку известна, мерили (DJI 2688x1512, HEVC +55 Мбит/с, 600 кадров, `--vo=null`): | | время | кадров в секунду | |---|---|---| | без фильтра | 1.82 с | 330 | | ветка «обрезать и растянуть» с `enable` | 2.81 с | 214 | -Нужно 60, то есть запас трёхкратный: путь через цепочку по деньгам -проходит. Раньше здесь было написано, что для 2688x1512 это уже не -пустяк, — написано по памяти о замере вписывания, а не об этом случае. - -Но есть путь дешевле. mpv умеет обрезать и увеличивать на выводе, силами -видеокарты: свойства `video-crop`, `video-zoom`, `video-pan-x/y`, -`video-rotate` есть в нашей версии 0.37. Они действуют на всё окно сразу, -но плеером управляем мы и знаем, где курсор: зашли в клип с кадрированием -— выставили, вышли — сняли. Ни цепочки, ни копирования кадров из памяти -видеокарты в обычную. Ограничения: одно преобразование на всё окно, так -что при одновременном наложении всё равно нужна цепочка; отражения таким -свойством не задать; переключение на границе клипа придёт с запозданием в -кадр-другой. +Нужно 60, то есть запас трёхкратный: по деньгам проходит. Устроена она +так: раздвоить поток, одну ветку обрезать и растянуть, и наложить её на +другую с `enable='between(t,...)'`. Иначе никак — из фильтров ffmpeg по +времени переключается только `overlay`, у `crop` и `scale` поддержки +временной шкалы нет. ## Два видео рядом в одном кадре diff --git a/README.md b/README.md index e6dfb90..92e26a0 100644 --- a/README.md +++ b/README.md @@ -403,11 +403,22 @@ Ctrl+Shift+R, который схлопывает дыру под курсоро Замер на материале 2688x1512 при 60 к/с: 327 кадров в секунду без фильтра, 230 с настоящим масштабированием — запас четырёхкратный. -Чего предпросмотр не умеет: показать собственную настройку кадрирования -файла. Цепочка фильтров у mpv одна на всю склейку, а заполнение с обрезкой, -поворот и смещение задаются для каждого файла отдельно — выбрать между ними -общий фильтр не может, поэтому он всегда вписывает целиком. Точную картинку -для такого файла показывает окно «Кадрирование и поворот». +**Кадрирование клипа окно показывает.** Цепочка фильтров у mpv одна на всю +склейку, а кадрирование у каждого клипа своё, и включать его по времени в +цепочке нечем: из фильтров ffmpeg по времени переключается только +наложение. Зато у вывода есть своя обрезка, и делает её видеокарта даром. +Курсор зашёл в клип со своим кадрированием — обрезка выставляется, вышел — +снимается. Ни лишней цепочки, ни копирования кадров из памяти видеокарты. + +Рамку считает один и тот же код, что рисует её в окне «Кадрирование и +поворот», — иначе окно обещало бы одно, а предпросмотр показывал другое +(`TransformSourceRect` в `uTransform`, проверка в `tests/lfmcheck`). + +Чего это не покрывает: отражение (свойства для него у вывода нет), +растяжение (там кадр жмётся неравномерно, рамкой не описать) и участки с +наложением — там кадр уже сведён цепочкой, и обрезка резала бы сведённое +целиком, вместе с нижним слоем. Для таких клипов точную картинку +по-прежнему показывает окно «Кадрирование и поворот». ## Цвета окна diff --git a/src/uFlatten.pas b/src/uFlatten.pas index 3011292..aa58bc5 100644 --- a/src/uFlatten.pas +++ b/src/uFlatten.pas @@ -167,6 +167,14 @@ function FlattenPreview(AProject: TProject; Возвращает, сколько секунд осталось. } +{ Клип, который виден в этот момент: с самой верхней включённой дорожки + нужного вида. Тот же отбор победителя, что и при уплощении. + + Нужен предпросмотру: по нему решается, чьё кадрирование показывать + обрезкой на выводе плеера. } +function VisibleClipAtTime(AProject: TProject; AKind: TTrackKind; + const ATime: Double): TClip; + function ClipSegmentsToRange(var ASegments: TFlatSegmentArray; const AFrom, ATo: Double): Double; @@ -341,6 +349,30 @@ begin end; end; +function VisibleClipAtTime(AProject: TProject; AKind: TTrackKind; + const ATime: Double): TClip; +var + Ti, C: Integer; + Track: TTrack; +begin + Result := nil; + if AProject = nil then + Exit; + for Ti := 0 to AProject.TrackCount - 1 do + begin + Track := AProject.Tracks[Ti]; + if Track.Kind <> AKind then + Continue; + { Выключенные дорожки пропускаются, а если где-то включено «соло» — + то и все несолирующие. } + if not AProject.TrackAudible(Track) then + Continue; + for C := 0 to Track.Count - 1 do + if Track[C].Covers(ATime) then + Exit(Track[C]); + end; +end; + function ClipSegmentsToRange(var ASegments: TFlatSegmentArray; const AFrom, ATo: Double): Double; var diff --git a/src/uMainForm.pas b/src/uMainForm.pas index baaa7f3..038b877 100644 --- a/src/uMainForm.pas +++ b/src/uMainForm.pas @@ -175,6 +175,8 @@ type FPreviewSegments: TFlatSegmentArray; FLastVolumeSent: Double; FLastVolumeKey: string; + { Что сейчас показано обрезкой на выводе: лишний раз её не трогаем. } + FLastCropKey: string; FGenerate: TGenerateThread; FGenerateBusy: Boolean; @@ -235,6 +237,8 @@ type FHintControl: TControl; procedure ApplyVolumeAt(const ATime: Double); + { Показывает кадрирование клипа, который виден сейчас. } + procedure ApplyCropAt(const ATime: Double); { Переписывает файл субтитров и отдаёт его плееру. } procedure UpdateSubtitles; procedure UpdateRippleButton; @@ -2404,6 +2408,15 @@ begin FSeekPending := True; FSeekWaitTicks := 0; + { Кадрирование выставляется заново после каждой загрузки. + + Обрезка на выводе — свойство плеера, а не списка кусков: она + переживает загрузку и осталась бы от прошлого монтажа. А при открытии + проекта курсор не двигается вовсе, и без этого вызова первый клип + показывался бы без своего кадрирования до первого щелчка по монтажу. } + FLastCropKey := ''; + ApplyCropAt(Pos); + SetStatus(Format('Предпросмотр: %d %s, %s с', [Length(ASegments), Plural(Length(ASegments), 'кусок', 'куска', 'кусков'), SecondsToStr(TotalDuration(ASegments))])); @@ -3447,6 +3460,75 @@ begin FPlayer.SetSubtitleFile(''); end; +{ Кадрирование клипа в предпросмотре. + + Цепочка фильтров у mpv одна на весь список кусков, а кадрирование у + каждого клипа своё, и включать его по времени в цепочке нечем: из + фильтров ffmpeg по времени переключается только наложение. Зато у + вывода есть своя обрезка, и делает её видеокарта даром. + + Раз она действует на весь вывод, переключать её обязаны мы: курсор + зашёл в клип со своим кадрированием — выставили, вышел — сняли. + + Участки с наложением сюда не попадают: там кадр уже сведён цепочкой, и + обрезка на выводе резала бы сведённое целиком, вместе с нижним слоем. + + Отражения обрезкой не выразить, растяжение — тоже: такие клипы + по-прежнему показываются как есть, точную картинку для них даёт окно + «Кадрирование и поворот». } +procedure TMainForm.ApplyCropAt(const ATime: Double); +var + I: Integer; + Clip: TClip; + Item: TMediaItem; + Sel: TRect; + Rotation: Integer; + Key: string; +begin + if (FPlayer = nil) or (not FPlayer.Initialized) or (FProject = nil) then + Exit; + + Sel := Rect(0, 0, 0, 0); + Rotation := 0; + + for I := 0 to High(FPreviewOverlayWindows) do + if (ATime >= FPreviewOverlayWindows[I].TlStart - 0.0005) and + (ATime <= FPreviewOverlayWindows[I].TlEnd + 0.0005) then + begin + FPlayer.SetOutputCrop(Sel, 0); + Exit; + end; + + Clip := VisibleClipAtTime(FProject, tkVideo, ATime); + if (Clip <> nil) and (Clip.Kind = ckVideo) and + TransformFitsOutputCrop(Clip.Transform) then + begin + Item := FProject.FindMedia(Clip.MediaId); + if (Item <> nil) and Item.Info.Valid and + (Item.Info.Width > 0) and (Item.Info.Height > 0) then + begin + Rotation := Clip.Transform.Rotation; + if not TransformSourceRect(Clip.Transform, + Item.Info.Width, Item.Info.Height, + FProject.Master.Width, FProject.Master.Height, Sel) then + Sel := Rect(0, 0, 0, 0) + { Рамка во весь кадр — это отсутствие обрезки. Ставить её значило бы + зря перестраивать вывод на каждом клипе. } + else if (Sel.Left <= 0) and (Sel.Top <= 0) and + (Sel.Right >= Item.Info.Width) and + (Sel.Bottom >= Item.Info.Height) then + Sel := Rect(0, 0, 0, 0); + end; + end; + + Key := Format('%d,%d,%d,%d,%d', + [Sel.Left, Sel.Top, Sel.Right, Sel.Bottom, Rotation]); + if Key = FLastCropKey then + Exit; + FLastCropKey := Key; + FPlayer.SetOutputCrop(Sel, Rotation); +end; + procedure TMainForm.ApplyVolumeAt(const ATime: Double); var I: Integer; @@ -3568,6 +3650,9 @@ end; procedure TMainForm.TimelinePlayheadChange(Sender: TObject); begin UpdateTimeLabel; + { Кадрирование показывается и на паузе: курсор переставили мышью — + картинка обязана смениться сразу, а не с началом воспроизведения. } + ApplyCropAt(FTimeline.Playhead); { Если курсор двинул сам плеер, перематывать его обратно нельзя — получится бесконечная переброска. } if FUpdatingFromPlayer then @@ -3622,6 +3707,7 @@ begin Exit; ApplyVolumeAt(FPlayer.TimePos); + ApplyCropAt(FPlayer.TimePos); FUpdatingFromPlayer := True; try diff --git a/src/uMpvPlayer.pas b/src/uMpvPlayer.pas index 8fdc762..9d7ebdd 100644 --- a/src/uMpvPlayer.pas +++ b/src/uMpvPlayer.pas @@ -70,6 +70,10 @@ type { Номера подключённых звуковых дорожек, в порядке подключения. } FAudioIds: array of string; FLastGraph: string; + { Что сейчас выставлено обрезкой на выводе. Хранится, чтобы не + переустанавливать свойство на каждом кадре. } + FLastCrop: string; + FLastRotate: string; FSubtitlePath: string; { Что произошло с выбором звуковой дорожки. Читается окном для журнала: без этого понять, откуда взялся звук, нечем. } @@ -147,6 +151,18 @@ type громкость текущего куска и есть громкость всего вывода. } procedure SetVolume(const APercent: Double); + { Обрезка на выводе: показывать только этот кусок кадра исходника. + + Делает её сама видеокарта, цепочки фильтров тут нет и кадры в + обычную память не копируются. Годится на то, чего цепочкой дорого: + показать собственное кадрирование клипа. Действует на весь вывод + сразу, поэтому включать и снимать её обязан тот, кто знает, где + сейчас курсор. + + Пустой прямоугольник снимает обрезку. Поворот задаётся тем же + способом — отдельным свойством, а не фильтром. } + procedure SetOutputCrop(const ARect: TRect; ARotation: Integer); + { Цепочка фильтров ffmpeg поверх всего, что играет. Нужна ровно для одного: показать кадр проекта, а не кадр исходника. @@ -727,6 +743,32 @@ begin mpv_set_property(FHandle, 'volume', MPV_FORMAT_DOUBLE, @V); end; +procedure TMpvPlayer.SetOutputCrop(const ARect: TRect; ARotation: Integer); +var + Value, Rot: string; +begin + if FHandle = nil then + Exit; + + if (ARect.Right > ARect.Left) and (ARect.Bottom > ARect.Top) then + Value := Format('%dx%d+%d+%d', + [ARect.Right - ARect.Left, ARect.Bottom - ARect.Top, + ARect.Left, ARect.Top]) + else + Value := ''; + Rot := IntToStr(((ARotation mod 360) + 360) mod 360); + + { Лишний раз не трогаем: смена свойства перестраивает вывод, и на + каждом кадре это было бы заметно глазом. } + if (Value = FLastCrop) and (Rot = FLastRotate) then + Exit; + FLastCrop := Value; + FLastRotate := Rot; + + mpv_set_property_string(FHandle, 'video-crop', PAnsiChar(Value)); + mpv_set_property_string(FHandle, 'video-rotate', PAnsiChar(Rot)); +end; + procedure TMpvPlayer.SetVideoFilter(const AFilter: string); var Value: string; diff --git a/src/uPanCropForm.pas b/src/uPanCropForm.pas index f5f961f..b251ca3 100644 --- a/src/uPanCropForm.pas +++ b/src/uPanCropForm.pas @@ -429,45 +429,14 @@ end; { Рамка по настройке. Обратное к TransformFromSel: там же и объяснение, откуда берутся числа. } procedure TPanCropForm.SelFromTransform; -var - KFit, KFill, K: Double; - SW, SH, CW, CH, CX, CY: Double; - W, H: Double; begin UpdateRotatedSize; if (FRotW <= 0) or (FRotH <= 0) or (FDstWidth <= 0) or (FDstHeight <= 0) then Exit; - - KFit := Min(FDstWidth / FRotW, FDstHeight / FRotH); - KFill := Max(FDstWidth / FRotW, FDstHeight / FRotH); - if FTransform.Fit = fmFill then - K := KFill - else - K := KFit; - if FTransform.Fit = fmStretch then - begin - { Растяжение рамкой не описывается: берётся весь кадр. } - FSel := Rect(0, 0, FRotW, FRotH); - Exit; - end; - if FTransform.Zoom > 0 then - K := K * FTransform.Zoom; - if K <= 0 then - Exit; - - SW := FRotW * K; - SH := FRotH * K; - CW := Min(SW, FDstWidth); - CH := Min(SH, FDstHeight); - CX := (SW - CW) / 2 + FTransform.OffsetX * FDstWidth; - CY := (SH - CH) / 2 + FTransform.OffsetY * FDstHeight; - CX := Max(0, Min(SW - CW, CX)); - CY := Max(0, Min(SH - CH, CY)); - - W := CW / K; - H := CH / K; - FSel := Rect(Round(CX / K), Round(CY / K), - Round(CX / K + W), Round(CY / K + H)); + { Считает uTransform: той же рамкой пользуется предпросмотр, отдавая её + плееру как обрезку на выводе. Двух расчётов быть не должно — они + разойдутся, и окно начнёт обещать не то, что покажет плеер. } + TransformSourceRect(FTransform, FRotW, FRotH, FDstWidth, FDstHeight, FSel); end; { Настройка по рамке. diff --git a/src/uTransform.pas b/src/uTransform.pas index acddee3..bbb07b1 100644 --- a/src/uTransform.pas +++ b/src/uTransform.pas @@ -82,6 +82,27 @@ function OverlayFilter(const AT: TMediaTransform; ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer; out AX, AY, AWidth, AHeight: Integer): string; +{ Какая часть исходника видна в кадре проекта — прямоугольник в точках + самого исходника. + + Та же настройка, записанная с другой стороны: вместо «как вписать» — + «что останется». Окно кадрирования рисует по ней рамку, а плеер отдаёт + её mpv как обрезку на выводе, и оба обязаны считать одинаково — иначе + окно обещает одно, а предпросмотр показывает другое. + + False — прямоугольником не описать: при растяжении берётся весь кадр и + сжимается неравномерно. } +function TransformSourceRect(const AT: TMediaTransform; + ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer; + out ARect: TRect): Boolean; + +{ Можно ли показать это преобразование обрезкой на выводе плеера. + + Обрезка умеет только выбрать кусок кадра. Поворот у mpv задаётся + отдельным свойством и потому разрешён, а отражение — нет: свойства для + него нет, и такой клип по-прежнему показывается как есть. } +function TransformFitsOutputCrop(const AT: TMediaTransform): Boolean; + { Фильтр цветового ключа: этот цвет становится прозрачным. Один на всех: и сборка, и предпросмотр должны выбивать одинаково, иначе @@ -153,6 +174,56 @@ begin (ASrcWidth = ADstWidth) and (ASrcHeight = ADstHeight); end; +function TransformSourceRect(const AT: TMediaTransform; + ASrcWidth, ASrcHeight, ADstWidth, ADstHeight: Integer; + out ARect: TRect): Boolean; +var + KFit, KFill, K: Double; + SW, SH, CW, CH, CX, CY: Double; +begin + ARect := Rect(0, 0, ASrcWidth, ASrcHeight); + Result := False; + if (ASrcWidth <= 0) or (ASrcHeight <= 0) or + (ADstWidth <= 0) or (ADstHeight <= 0) then + Exit; + { Растяжение рамкой не описывается: берётся весь кадр и жмётся + неравномерно. } + if AT.Fit = fmStretch then + Exit; + + 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; + if K <= 0 then + Exit; + + { Кадр, растянутый до рабочего масштаба, и та его часть, что помещается + в кадр проекта. Смещение задаётся в долях кадра проекта и прижимается + к краям: выйти за пределы картинки нельзя. } + SW := ASrcWidth * K; + SH := ASrcHeight * K; + CW := Min(SW, ADstWidth); + CH := Min(SH, ADstHeight); + CX := (SW - CW) / 2 + AT.OffsetX * ADstWidth; + CY := (SH - CH) / 2 + AT.OffsetY * ADstHeight; + CX := Max(0, Min(SW - CW, CX)); + CY := Max(0, Min(SH - CH, CY)); + + ARect := Rect(Round(CX / K), Round(CY / K), + Round((CX + CW) / K), Round((CY + CH) / K)); + Result := True; +end; + +function TransformFitsOutputCrop(const AT: TMediaTransform): Boolean; +begin + Result := (not AT.FlipH) and (not AT.FlipV) and (AT.Fit <> fmStretch); +end; + { Дробное число через точку: у ffmpeg разделитель всегда точка, а в русской локали Format вставил бы запятую и развалил бы всю цепочку. } function FilterNum(const AValue: Double): string; diff --git a/tests/lfmcheck/lfmcheck.lpr b/tests/lfmcheck/lfmcheck.lpr index e9cba41..b9d1dca 100644 --- a/tests/lfmcheck/lfmcheck.lpr +++ b/tests/lfmcheck/lfmcheck.lpr @@ -786,6 +786,64 @@ begin Inc(ClickCount); end; +{ Рамка источника и фильтр сборки должны говорить об одном. + + Одна и та же настройка записана двумя способами: сборка растягивает + кадр и вырезает из него кусок, а окно кадрирования и предпросмотр + говорят, какая часть исходника видна. Разойдутся — окно начнёт обещать + не то, что покажет плеер, а плеер не то, что даст сборка. } +procedure CheckSourceRect; +var + T: TMediaTransform; + R: TRect; + Chain: string; +begin + WriteLn; + WriteLn('Рамка источника и фильтр сборки'); + WriteLn; + + T := DefaultTransform; + T.Zoom := 2.77; + + CheckTrue('увеличение описывается рамкой', + TransformSourceRect(T, 2688, 1512, 2688, 1512, R)); + { Растянули в 2.77 раза — значит видна 1/2.77 часть кадра, посередине. } + CheckTrue('ширина рамки: ' + IntToStr(R.Right - R.Left), + Abs((R.Right - R.Left) - Round(2688 / 2.77)) <= 2); + CheckTrue('высота рамки: ' + IntToStr(R.Bottom - R.Top), + Abs((R.Bottom - R.Top) - Round(1512 / 2.77)) <= 2); + CheckTrue('рамка посередине по горизонтали', + Abs((R.Left + R.Right) - 2688) <= 3); + CheckTrue('рамка посередине по вертикали', + Abs((R.Top + R.Bottom) - 1512) <= 3); + + { Тот же расчёт с другой стороны: сборка растягивает до 7446x4188 и + вырезает 2688x1512 с отступом 2379:1338. Делим отступ на масштаб — + обязано выйти начало рамки. } + Chain := TransformFilter(T, 2688, 1512, 2688, 1512); + CheckTrue('сборка растягивает и вырезает: ' + Chain, + (Pos('scale=7446:4188', Chain) > 0) and + (Pos('crop=2688:1512:2379:1338', Chain) > 0)); + CheckTrue('начало рамки совпадает с отступом обрезки', + Abs(R.Left - Round(2379 / 2.77)) <= 2); + + { Растяжение рамкой не описывается: там кадр жмётся неравномерно. } + T := DefaultTransform; + T.Fit := fmStretch; + CheckTrue('растяжение рамкой не описывается', + not TransformSourceRect(T, 2688, 1512, 1920, 1080, R)); + + { Отражение обрезкой на выводе не показать — свойства для него нет. } + T := DefaultTransform; + T.FlipH := True; + CheckTrue('отражение обрезкой на выводе не показывается', + not TransformFitsOutputCrop(T)); + T := DefaultTransform; + T.Rotation := 90; + CheckTrue('поворот показать можно: у него своё свойство', + TransformFitsOutputCrop(T)); +end; + procedure CheckButtonClicks; var Btn: TButtonProbe; @@ -845,6 +903,7 @@ begin CheckSplitAndPlayhead; CheckSplitAllTracks; CheckGapTolerance; + CheckSourceRect; CheckButtonClicks; WriteLn;