diff --git a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java index 7aa83960199..0ae5b3cd6fd 100644 --- a/src/main/java/org/prebid/server/auction/InterstitialProcessor.java +++ b/src/main/java/org/prebid/server/auction/InterstitialProcessor.java @@ -5,15 +5,22 @@ import com.iab.openrtb.request.Device; import com.iab.openrtb.request.Format; import com.iab.openrtb.request.Imp; +import lombok.Value; import org.apache.commons.collections4.CollectionUtils; +import org.apache.commons.lang3.BooleanUtils; import org.prebid.server.exception.InvalidRequestException; import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSdk; +import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.stream.Stream; public class InterstitialProcessor { @@ -36,15 +43,28 @@ private BidRequest processBidRequest(BidRequest bidRequest) { if (extDeviceInt != null) { final int minWidthPerc = extDeviceInt.getMinWidthPerc(); final int minHeightPerc = extDeviceInt.getMinHeightPerc(); + final boolean usePxRatio = usePxRatio(bidRequest); final List updatedImps = bidRequest.getImp().stream() - .map(imp -> processInterstitialImp(imp, device, minWidthPerc, minHeightPerc)) + .map(imp -> processInterstitialImp(imp, device, minWidthPerc, minHeightPerc, usePxRatio)) .toList(); bidRequest = bidRequest.toBuilder().imp(updatedImps).build(); } return bidRequest; } - private Imp processInterstitialImp(Imp imp, Device device, int minWidthPerc, int minHeightPerc) { + private static boolean usePxRatio(BidRequest bidRequest) { + final ExtRequest extRequest = bidRequest.getExt(); + final ExtRequestPrebid prebid = extRequest != null ? extRequest.getPrebid() : null; + final ExtRequestPrebidSdk sdk = prebid != null ? prebid.getSdk() : null; + return sdk != null && BooleanUtils.isTrue(sdk.getUsePxRatio()); + } + + private Imp processInterstitialImp(Imp imp, + Device device, + int minWidthPerc, + int minHeightPerc, + boolean usePxRatio) { + if (!isInterstitial(imp)) { return imp; } @@ -54,36 +74,58 @@ private Imp processInterstitialImp(Imp imp, Device device, int minWidthPerc, int return imp; } + final InterstitialSize maxSize = getMaxSize(banner, device, imp.getId(), usePxRatio); + final double minWidth = getMinSize(maxSize.getW(), minWidthPerc); + final double minHeight = getMinSize(maxSize.getH(), minHeightPerc); + + final List interstitialFormats = InterstitialSize.getNestedSizes(minWidth, minHeight, maxSize) + .map(size -> Format.builder().w(size.w).h(size.h).build()) + .toList(); + + return CollectionUtils.isNotEmpty(interstitialFormats) + ? imp.toBuilder().banner(banner.toBuilder().format(interstitialFormats).build()).build() + : imp; + } + + private static InterstitialSize getMaxSize(Banner banner, Device device, String impId, boolean usePxRatio) { final List formats = banner.getFormat(); final Format firstFormat = CollectionUtils.isEmpty(formats) ? null : formats.getFirst(); - Integer maxHeight = firstFormat != null ? firstFormat.getH() : null; - Integer maxWidth = firstFormat != null ? firstFormat.getW() : null; + final Integer firstFormatWidth = firstFormat != null ? firstFormat.getW() : null; + final Integer firstFormatHeight = firstFormat != null ? firstFormat.getH() : null; + + if (firstFormatWidth != null + && firstFormatHeight != null + && (firstFormatWidth != 1 || firstFormatHeight != 1)) { - if (maxHeight == null || maxWidth == null || (maxHeight == 1 && maxWidth == 1)) { - maxHeight = device.getH(); - maxWidth = device.getW(); + return InterstitialSize.interstitialSize(firstFormatWidth, firstFormatHeight); } - if (maxHeight == null || maxWidth == null) { + final Integer deviceWidth = device.getW(); + final Integer deviceHeight = device.getH(); + if (deviceWidth == null || deviceHeight == null) { throw new InvalidRequestException( "Unable to read max interstitial size for Imp id=%s (No Device sizes and no Format objects)" - .formatted(imp.getId())); + .formatted(impId)); } - final double minHeight = (double) maxHeight / 100 * minHeightPerc; - final double minWidth = (double) maxWidth / 100 * minWidthPerc; + if (usePxRatio) { + final BigDecimal pxRatio = device.getPxratio(); + return InterstitialSize.interstitialSize( + deviceSizeToDips(deviceWidth, pxRatio), + deviceSizeToDips(deviceHeight, pxRatio)); + } - final List interstitialFormats = - InterstitialSize.getNestedSizes(minWidth, minHeight, maxWidth, maxHeight, MAX_SIZES_COUNT) - .stream() - .map(interstitialSize -> Format.builder().w(interstitialSize.w).h(interstitialSize.h).build()) - .toList(); + return InterstitialSize.interstitialSize(deviceWidth, deviceHeight); + } - if (CollectionUtils.isEmpty(interstitialFormats)) { - return imp; - } + private static int deviceSizeToDips(int size, BigDecimal pxRatio) { + return pxRatio != null && pxRatio.signum() > 0 + ? Math.max(1, (int) Math.round(size / pxRatio.doubleValue())) + : size; + } - return imp.toBuilder().banner(banner.toBuilder().format(interstitialFormats).build()).build(); + private static double getMinSize(int maxSize, int minSizePerc) { + return maxSize / 100.0 * minSizePerc; } private ExtDeviceInt getExtDeviceInt(Device device) { @@ -92,6 +134,7 @@ private ExtDeviceInt getExtDeviceInt(Device device) { return extDevicePrebid != null ? extDevicePrebid.getInterstitial() : null; } + @Value(staticConstructor = "interstitialSize") private static class InterstitialSize { private static final List INTERSTITIAL_SIZES = new ArrayList<>(); @@ -349,33 +392,19 @@ private static class InterstitialSize { INTERSTITIAL_SIZES.add(interstitialSize(120, 20)); } - private final Integer w; - private final Integer h; - - private InterstitialSize(Integer w, Integer h) { - this.w = w; - this.h = h; - } - - private static InterstitialSize interstitialSize(Integer w, Integer h) { - return new InterstitialSize(w, h); - } - - private static List getNestedSizes(double minWidth, - double minHeight, - double maxWidth, - double maxHeight, - int count) { + int w; + int h; + private static Stream getNestedSizes(double minWidth, double minHeight, + InterstitialSize max) { return INTERSTITIAL_SIZES.stream() - .filter(size -> isNested(size, minWidth, minHeight, maxWidth, maxHeight)) - .limit(count) - .toList(); + .filter(size -> isNested(size, minWidth, minHeight, max)) + .limit(MAX_SIZES_COUNT); } - private static boolean isNested(InterstitialSize size, double minWidth, double minHeight, double maxWidth, - double maxHeight) { - return size.w >= minWidth && size.w <= maxWidth && size.h >= minHeight && size.h <= maxHeight; + private static boolean isNested(InterstitialSize size, double minWidth, double minHeight, + InterstitialSize max) { + return size.w >= minWidth && size.w <= max.w && size.h >= minHeight && size.h <= max.h; } } } diff --git a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSdk.java b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSdk.java index fe6ae9d55f8..cff39b5a778 100644 --- a/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSdk.java +++ b/src/main/java/org/prebid/server/proto/openrtb/ext/request/ExtRequestPrebidSdk.java @@ -1,5 +1,6 @@ package org.prebid.server.proto.openrtb.ext.request; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Value; import java.util.List; @@ -7,11 +8,21 @@ /** * Defines the contract for bidrequest.ext.prebid.sdk */ -@Value(staticConstructor = "of") +@Value public class ExtRequestPrebidSdk { /** * Defines the contract for bidrequest.ext.prebid.sdk.renderers */ List renderers; + + /** + * Defines the contract for bidrequest.ext.prebid.sdk.usepxratio + */ + @JsonProperty("usepxratio") + Boolean usePxRatio; + + public static ExtRequestPrebidSdk of(List renderers, Boolean usePxRatio) { + return new ExtRequestPrebidSdk(renderers, usePxRatio); + } } diff --git a/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java b/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java index 662c58e7aa8..123bfcedb3b 100644 --- a/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java +++ b/src/test/java/org/prebid/server/auction/InterstitialProcessorTest.java @@ -11,6 +11,11 @@ import org.prebid.server.proto.openrtb.ext.request.ExtDevice; import org.prebid.server.proto.openrtb.ext.request.ExtDeviceInt; import org.prebid.server.proto.openrtb.ext.request.ExtDevicePrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequest; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebid; +import org.prebid.server.proto.openrtb.ext.request.ExtRequestPrebidSdk; + +import java.math.BigDecimal; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; @@ -40,7 +45,7 @@ public void processShouldReturnBidRequestUpdatedWithImpsInterstitialFormat() { assertThat(result.getImp()) .extracting(Imp::getBanner) .flatExtracting(Banner::getFormat) - .containsOnly(Format.builder().w(320).h(480).build(), + .containsExactly(Format.builder().w(320).h(480).build(), Format.builder().w(336).h(544).build(), Format.builder().w(320).h(568).build(), Format.builder().w(320).h(500).build(), @@ -64,13 +69,46 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSize assertThat(result.getImp()) .extracting(Imp::getBanner) .flatExtracting(Banner::getFormat) - .containsOnly(Format.builder().w(320).h(480).build(), + .containsExactly(Format.builder().w(320).h(480).build(), Format.builder().w(336).h(544).build(), Format.builder().w(320).h(568).build(), Format.builder().w(320).h(500).build(), Format.builder().w(320).h(481).build()); } + @Test + public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingDeviceSizeInDipsWhenFormatIsEmpty() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(1).build())) + .device(Device.builder() + .w(1080) + .h(1920) + .pxratio(BigDecimal.valueOf(3)) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(60, 60)))) + .build()) + .ext(usePxRatioExt()) + .build(); + + // when + final BidRequest result = interstitialProcessor.process(bidRequest); + + // then + assertThat(result.getImp()) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly(Format.builder().w(300).h(600).build(), + Format.builder().w(320).h(480).build(), + Format.builder().w(250).h(600).build(), + Format.builder().w(360).h(640).build(), + Format.builder().w(320).h(640).build(), + Format.builder().w(300).h(480).build(), + Format.builder().w(336).h(544).build(), + Format.builder().w(303).h(603).build(), + Format.builder().w(320).h(568).build(), + Format.builder().w(301).h(601).build()); + } + @Test public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSizesFromDeviceWhenFormatIsOneToOne() { // given @@ -89,13 +127,166 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingSize assertThat(result.getImp()) .extracting(Imp::getBanner) .flatExtracting(Banner::getFormat) - .containsOnly(Format.builder().w(320).h(480).build(), + .containsExactly(Format.builder().w(320).h(480).build(), Format.builder().w(336).h(544).build(), Format.builder().w(320).h(568).build(), Format.builder().w(320).h(500).build(), Format.builder().w(320).h(481).build()); } + @Test + public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsUsingDeviceSizeInDipsWhenFormatIsOneToOne() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder().format(singletonList( + Format.builder().w(1).h(1).build())).build()).instl(1).build())) + .device(Device.builder() + .w(1080) + .h(1920) + .pxratio(BigDecimal.valueOf(3)) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(60, 60)))) + .build()) + .ext(usePxRatioExt()) + .build(); + + // when + final BidRequest result = interstitialProcessor.process(bidRequest); + + // then + assertThat(result.getImp()) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly(Format.builder().w(300).h(600).build(), + Format.builder().w(320).h(480).build(), + Format.builder().w(250).h(600).build(), + Format.builder().w(360).h(640).build(), + Format.builder().w(320).h(640).build(), + Format.builder().w(300).h(480).build(), + Format.builder().w(336).h(544).build(), + Format.builder().w(303).h(603).build(), + Format.builder().w(320).h(568).build(), + Format.builder().w(301).h(601).build()); + } + + @Test + public void processShouldNotConvertExplicitFormatSizeUsingDevicePxratio() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder() + .format(singletonList(Format.builder().w(400).h(600).build())).build()).instl(1) + .build())) + .device(Device.builder() + .w(1080) + .h(1920) + .pxratio(BigDecimal.valueOf(3)) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .build()) + .ext(usePxRatioExt()) + .build(); + + // when + final BidRequest result = interstitialProcessor.process(bidRequest); + + // then + assertThat(result.getImp()) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly(Format.builder().w(320).h(480).build(), + Format.builder().w(336).h(544).build(), + Format.builder().w(320).h(568).build(), + Format.builder().w(320).h(500).build(), + Format.builder().w(320).h(481).build()); + } + + @Test + public void processShouldKeepCurrentDeviceSizeBehaviorWhenUsePxRatioIsTrueAndDevicePxRatioIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(1).build())) + .device(Device.builder() + .w(1080) + .h(1920) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(1, 1)))) + .build()) + .ext(usePxRatioExt()) + .build(); + + // when + final BidRequest result = interstitialProcessor.process(bidRequest); + + // then + assertThat(result.getImp()) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly(Format.builder().w(300).h(250).build(), + Format.builder().w(728).h(90).build(), + Format.builder().w(160).h(600).build(), + Format.builder().w(320).h(50).build(), + Format.builder().w(300).h(600).build(), + Format.builder().w(970).h(250).build(), + Format.builder().w(970).h(1000).build(), + Format.builder().w(320).h(320).build(), + Format.builder().w(768).h(1024).build(), + Format.builder().w(1024).h(768).build()); + } + + @Test + public void processShouldKeepCurrentDeviceSizeBehaviorWhenUsePxRatioIsAbsent() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder().build()).instl(1).build())) + .device(Device.builder() + .w(1080) + .h(1920) + .pxratio(BigDecimal.valueOf(3)) + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(1, 1)))) + .build()) + .build(); + + // when + final BidRequest result = interstitialProcessor.process(bidRequest); + + // then + assertThat(result.getImp()) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly(Format.builder().w(300).h(250).build(), + Format.builder().w(728).h(90).build(), + Format.builder().w(160).h(600).build(), + Format.builder().w(320).h(50).build(), + Format.builder().w(300).h(600).build(), + Format.builder().w(970).h(250).build(), + Format.builder().w(970).h(1000).build(), + Format.builder().w(320).h(320).build(), + Format.builder().w(768).h(1024).build(), + Format.builder().w(1024).h(768).build()); + } + + @Test + public void processShouldNotTruncateMinimumInterstitialSizeThreshold() { + // given + final BidRequest bidRequest = BidRequest.builder() + .imp(singletonList(Imp.builder().banner(Banner.builder() + .format(singletonList(Format.builder().w(301).h(500).build())).build()).instl(1) + .build())) + .device(Device.builder() + .ext(ExtDevice.of(null, ExtDevicePrebid.of(ExtDeviceInt.of(80, 80)))) + .build()) + .build(); + + // when + final BidRequest result = interstitialProcessor.process(bidRequest); + + // then + assertThat(result.getImp()) + .extracting(Imp::getBanner) + .flatExtracting(Banner::getFormat) + .containsExactly(Format.builder().w(300).h(480).build(), + Format.builder().w(300).h(500).build(), + Format.builder().w(300).h(431).build(), + Format.builder().w(300).h(430).build()); + } + @Test public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsLimitedByTen() { // given @@ -115,7 +306,7 @@ public void processShouldReturnBidRequestUpdatedWithInterstitialFormatsLimitedBy assertThat(result.getImp()) .extracting(Imp::getBanner) .flatExtracting(Banner::getFormat) - .containsOnly(Format.builder().w(300).h(250).build(), + .containsExactly(Format.builder().w(300).h(250).build(), Format.builder().w(160).h(600).build(), Format.builder().w(320).h(50).build(), Format.builder().w(300).h(600).build(), @@ -266,4 +457,10 @@ public void processShouldNotUpdateImpWhenInterstitialSizesWereNotFound() { .build()) .build()); } + + private static ExtRequest usePxRatioExt() { + return ExtRequest.of(ExtRequestPrebid.builder() + .sdk(ExtRequestPrebidSdk.of(null, true)) + .build()); + } } diff --git a/src/test/java/org/prebid/server/bidder/nativo/NativoBidderTest.java b/src/test/java/org/prebid/server/bidder/nativo/NativoBidderTest.java index f9bd907ef85..a99552283f0 100644 --- a/src/test/java/org/prebid/server/bidder/nativo/NativoBidderTest.java +++ b/src/test/java/org/prebid/server/bidder/nativo/NativoBidderTest.java @@ -276,7 +276,7 @@ private static BidRequest givenBidRequestWithRenderer(UnaryOperator