You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

222 lines
10 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import CoreMedia
import XCTest
@testable import DropTwoCore
final class CropEngineTests: XCTestCase {
private var tempDirectory: URL!
override func setUpWithError() throws {
tempDirectory = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString)
try FileManager.default.createDirectory(at: tempDirectory, withIntermediateDirectories: true)
}
override func tearDownWithError() throws {
try? FileManager.default.removeItem(at: tempDirectory)
}
// MARK: - Geometrie (rein arithmetisch, kein Video nötig)
func testTargetSizeMatchesReadmeExamples() {
XCTAssertEqual(
CropEngine.targetSize(forSourceSize: CGSize(width: 1080, height: 1920)),
CGSize(width: 1080, height: 1350)
)
XCTAssertEqual(
CropEngine.targetSize(forSourceSize: CGSize(width: 2160, height: 3840)),
CGSize(width: 2160, height: 2700)
)
}
func testTargetSizeRoundsHeightToEvenNumber() {
let size = CropEngine.targetSize(forSourceSize: CGSize(width: 217, height: 386))
XCTAssertEqual(size.width, 217)
XCTAssertEqual(size.height.truncatingRemainder(dividingBy: 2), 0)
}
func testCropRectMapsOffsetToTopCenterBottom() {
let source = CGSize(width: 1080, height: 1920)
XCTAssertEqual(CropEngine.cropRect(forSourceSize: source, offset: -1).minY, 0)
XCTAssertEqual(CropEngine.cropRect(forSourceSize: source, offset: 0).minY, 286)
XCTAssertEqual(CropEngine.cropRect(forSourceSize: source, offset: 1).maxY, 1920)
XCTAssertEqual(CropEngine.cropRect(forSourceSize: source, offset: 5).maxY, 1920)
}
func testCropRectTopEdgeIsEven() {
let rect = CropEngine.cropRect(forSourceSize: CGSize(width: 1080, height: 1920), offset: 0.3333)
XCTAssertEqual(rect.minY.truncatingRemainder(dividingBy: 2), 0)
}
// MARK: - End-to-End gegen einen synthetischen Clip
/// Quelle: obere Hälfte weiß, untere schwarz. Ausschnitt ganz oben muss
/// also deutlich heller sein als ganz unten — prüft die Richtung des
/// Offsets im tatsächlichen Render, nicht nur in der Geometrie.
func testOffsetMovesRenderedCropUpAndDown() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(includeAudio: false, pattern: .brightTopDarkBottom, in: tempDirectory)
let top = tempDirectory.appendingPathComponent("top.mp4")
let bottom = tempDirectory.appendingPathComponent("bottom.mp4")
try await CropEngine.crop(sourceURL: clip.url, destinationURL: top, options: .init(cropPlan: CropPlan(offset: -1)))
try await CropEngine.crop(sourceURL: clip.url, destinationURL: bottom, options: .init(cropPlan: CropPlan(offset: 1)))
let topBrightness = try await TestVideoFactory.averageBrightness(of: top, at: 0.5)
let bottomBrightness = try await TestVideoFactory.averageBrightness(of: bottom, at: 0.5)
// 384er Quelle, 270er Ausschnitt: oben 192/270 ≈ 0.71 weiß, unten 78/270 ≈ 0.29.
XCTAssertEqual(topBrightness, 0.71, accuracy: 0.08)
XCTAssertEqual(bottomBrightness, 0.29, accuracy: 0.08)
}
/// iPhone-Fall: Container quer (384×216), per 90°-Transformation hochkant.
/// Links im gespeicherten Bild ist nach der Drehung oben — der Ausschnitt
/// "ganz oben" muss also hell sein, und das Ergebnis darf nicht quer sein.
func testRotatedSourceIsCroppedUpright() async throws {
let clip = try TestVideoFactory.makeClip(
size: CGSize(width: 384, height: 216),
includeAudio: false,
pattern: .brightLeftDarkRight,
transform: CGAffineTransform(a: 0, b: 1, c: -1, d: 0, tx: 216, ty: 0),
in: tempDirectory
)
let top = tempDirectory.appendingPathComponent("top.mp4")
let bottom = tempDirectory.appendingPathComponent("bottom.mp4")
try await CropEngine.crop(sourceURL: clip.url, destinationURL: top, options: .init(cropPlan: CropPlan(offset: -1)))
try await CropEngine.crop(sourceURL: clip.url, destinationURL: bottom, options: .init(cropPlan: CropPlan(offset: 1)))
let resultInfo = try await VideoInspector.inspect(top)
XCTAssertEqual(resultInfo.pixelSize, CGSize(width: 216, height: 270))
let topBrightness = try await TestVideoFactory.averageBrightness(of: top, at: 0.5)
let bottomBrightness = try await TestVideoFactory.averageBrightness(of: bottom, at: 0.5)
XCTAssertEqual(topBrightness, 0.71, accuracy: 0.08)
XCTAssertEqual(bottomBrightness, 0.29, accuracy: 0.08)
}
func testOrientationFromTrackTransform() {
XCTAssertEqual(CropEngine.orientation(for: .identity), .up)
XCTAssertEqual(CropEngine.orientation(for: CGAffineTransform(a: 0, b: 1, c: -1, d: 0, tx: 1080, ty: 0)), .right)
XCTAssertEqual(CropEngine.orientation(for: CGAffineTransform(a: 0, b: -1, c: 1, d: 0, tx: 0, ty: 1920)), .left)
XCTAssertEqual(CropEngine.orientation(for: CGAffineTransform(a: -1, b: 0, c: 0, d: -1, tx: 1080, ty: 1920)), .down)
}
func testKeyframeSwitchesCropMidClip() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(includeAudio: false, pattern: .brightTopDarkBottom, in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
var plan = CropPlan(offset: -1)
plan.addKeyframe(at: 1)
plan.setOffset(1, at: 1)
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination, options: .init(cropPlan: plan))
let before = try await TestVideoFactory.averageBrightness(of: destination, at: 0.5)
let after = try await TestVideoFactory.averageBrightness(of: destination, at: 1.5)
XCTAssertEqual(before, 0.71, accuracy: 0.08)
XCTAssertEqual(after, 0.29, accuracy: 0.08)
}
func testCropsToTargetSizeAndKeepsAudioAndDuration() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination)
let resultInfo = try await VideoInspector.inspect(destination)
let expectedSize = CropEngine.targetSize(forSourceSize: clip.size)
XCTAssertEqual(resultInfo.pixelSize, expectedSize)
XCTAssertEqual(resultInfo.codec, .h264)
XCTAssertTrue(resultInfo.hasAudioTrack)
XCTAssertEqual(CMTimeGetSeconds(resultInfo.duration), clip.seconds, accuracy: 0.3)
}
func testCropsWithoutAudioTrack() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(includeAudio: false, in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination)
let resultInfo = try await VideoInspector.inspect(destination)
XCTAssertFalse(resultInfo.hasAudioTrack)
}
func testReportsProgress() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
var reportedValues: [Double] = []
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination) { value in
reportedValues.append(value)
}
XCTAssertFalse(reportedValues.isEmpty)
XCTAssertEqual(reportedValues.last ?? 0, 1.0, accuracy: 0.15)
}
// MARK: - Ablehnungen
func testRejectsNonNineBySixteenSource() async throws {
let clip = try TestVideoFactory.makeClip(size: CGSize(width: 400, height: 400), in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
do {
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination)
XCTFail("Erwartete Ablehnung für eine Nicht-9:16-Quelle")
} catch CropEngineError.sourceNotNineBySixteen {
// erwartet
}
}
func testRejectsExistingDestinationWithoutOverwrite() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
FileManager.default.createFile(atPath: destination.path, contents: Data())
do {
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination)
XCTFail("Erwartete Ablehnung wegen bestehender Zieldatei")
} catch CropEngineError.destinationAlreadyExists {
// erwartet
}
}
func testOverwritesExistingDestinationWhenExplicitlyAllowed() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mp4")
FileManager.default.createFile(atPath: destination.path, contents: Data("platzhalter".utf8))
try await CropEngine.crop(
sourceURL: clip.url,
destinationURL: destination,
options: .init(overwrite: true)
)
let resultInfo = try await VideoInspector.inspect(destination)
XCTAssertEqual(resultInfo.pixelSize, CropEngine.targetSize(forSourceSize: clip.size))
}
func testRejectsUnsupportedContainer() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mxf")
do {
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination)
XCTFail("Erwartete Ablehnung für ein MXF-Ziel")
} catch CropEngineError.unsupportedContainer {
// erwartet
}
}
func testRejectsMovAsDestinationBecauseTargetIsFixedToMp4() async throws {
let clip = try TestVideoFactory.makeNineBySixteenClip(in: tempDirectory)
let destination = tempDirectory.appendingPathComponent("out.mov")
do {
try await CropEngine.crop(sourceURL: clip.url, destinationURL: destination)
XCTFail("Erwartete Ablehnung für ein MOV-Ziel — Zielformat ist fest auf MP4 gesetzt")
} catch CropEngineError.unsupportedContainer {
// erwartet
}
}
}

Powered by TurnKey Linux.