diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index 988169287..f8d0082e8 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -16,12 +16,14 @@ jobs: with: submodules: recursive fetch-depth: 0 - - name: ccache uses: hendrikmuhs/ccache-action@v1.2 with: key: macos + - name: install jack + run: brew install jack + - name: Create Build Environment run: cmake -E make_directory ${{github.workspace}}/build @@ -99,6 +101,9 @@ jobs: with: key: macos-legacy + - name: install jack + run: brew install jack + - name: Create Build Environment run: cmake -E make_directory ${{github.workspace}}/build @@ -170,6 +175,12 @@ jobs: submodules: recursive fetch-depth: 0 + - name: Install winget + uses: Cyberboss/install-winget@v1 + + - name: Install jack + run: winget install jack2 --disable-interactivity --accept-source-agreements + - name: Create Build Environment run: cmake -E make_directory ${{github.workspace}}/build @@ -225,6 +236,12 @@ jobs: submodules: recursive fetch-depth: 0 + - name: Install winget + uses: Cyberboss/install-winget@v1 + + - name: Install jack + run: winget install jack2 --disable-interactivity --accept-source-agreements + - name: Create Build Environment run: cmake -E make_directory ${{github.workspace}}/build diff --git a/CMakeLists.txt b/CMakeLists.txt index d620f792f..5f7dbbcbd 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -331,6 +331,20 @@ include_directories(/usr/local/include) link_directories(/usr/local/lib) endif() +# macOS includes for jack +if(APPLE) + if(MACOS_LEGACY) + include_directories(/usr/local/include) + else() + include_directories(/opt/homebrew/include) + endif() +endif() + +# Windows include for jack +if(MSVC) + include_directories("C:\\Program Files\\JACK2\\include") +endif() + list(APPEND PLUGDATA_COMPILE_DEFINITIONS JUCE_MODAL_LOOPS_PERMITTED=1) list(APPEND PLUGDATA_INCLUDE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/Libraries/ELSE/sfont~/") diff --git a/Libraries/JUCE b/Libraries/JUCE index d924b5908..60f33c4af 160000 --- a/Libraries/JUCE +++ b/Libraries/JUCE @@ -1 +1 @@ -Subproject commit d924b5908ccc6b9f857b0d8d488304c4eaaf1f1d +Subproject commit 60f33c4afdedf6da2b1fc84720b6cae6cb6eceb6 diff --git a/Libraries/pd-else b/Libraries/pd-else index 66b483943..c5baf73e4 160000 --- a/Libraries/pd-else +++ b/Libraries/pd-else @@ -1 +1 @@ -Subproject commit 66b483943e32344a7d143160db1f6dc1f2aaff42 +Subproject commit c5baf73e406e35778fde945d1ac1a8256221a80b diff --git a/Resources/Fonts/IconFont.ttf b/Resources/Fonts/IconFont.ttf index e9856e9be..58dccbc55 100644 Binary files a/Resources/Fonts/IconFont.ttf and b/Resources/Fonts/IconFont.ttf differ diff --git a/Source/Canvas.cpp b/Source/Canvas.cpp index 471e51e9f..8341c1978 100644 --- a/Source/Canvas.cpp +++ b/Source/Canvas.cpp @@ -316,6 +316,13 @@ Canvas::Canvas(PluginEditor* parent, pd::Patch::Ptr p, Component* parentGraph) commandLocked.referTo(pd->commandLocked); commandLocked.addListener(this); + // pd->commandLocked doesn't get updated when a canvas isn't active + // So we set it to false here when a canvas is remade + // Otherwise the last canvas could have set it true, and it would still be + // in that state without command actually being locked + if (!isGraph) + commandLocked.setValue(false); + // init border for testing settingsChanged("border", SettingsFile::getInstance()->getPropertyAsValue("border")); @@ -1320,7 +1327,9 @@ void Canvas::middleMouseChanged(bool isHeld) void Canvas::altKeyChanged(bool isHeld) { - SettingsFile::getInstance()->getValueTree().getChildWithName("Overlays").setProperty("alt_mode", isHeld, nullptr); + if(!isGraph) { + SettingsFile::getInstance()->getValueTree().getChildWithName("Overlays").setProperty("alt_mode", isHeld, nullptr); + } } void Canvas::mouseDown(MouseEvent const& e) diff --git a/Source/Components/DraggableNumber.h b/Source/Components/DraggableNumber.h index 09fb755d3..2c44ef223 100644 --- a/Source/Components/DraggableNumber.h +++ b/Source/Components/DraggableNumber.h @@ -54,6 +54,7 @@ class DraggableNumber : public Label addListener(this); setFont(Fonts::getTabularNumbersFont().withHeight(14.0f)); lookAndFeelChanged(); + setInterceptsMouseClicks(true, true); } void colourChanged() override @@ -104,10 +105,10 @@ class DraggableNumber : public Label onInteraction(false); } - void setEditableOnClick(bool editable) + void setEditableOnClick(bool editable, bool handleFocusLossManually = false) { - setEditable(editable, editable); - setInterceptsMouseClicks(true, true); + setEditable(editable, editable, handleFocusLossManually); + setWantsKeyboardFocus(true); } void setMaximum(double maximum) @@ -589,14 +590,14 @@ class DraggableNumber : public Label void textEditorFocusLost (TextEditor& editor) override { - textEditorReturnKeyPressed(editor); + //hideEditor(false); } void textEditorReturnKeyPressed(TextEditor& editor) override { auto text = editor.getText(); double newValue = parseExpression(text); - setValue(newValue); + setValue(newValue, dontSendNotification); onReturnKey(newValue); } }; @@ -610,7 +611,7 @@ struct DraggableListNumber : public DraggableNumber { explicit DraggableListNumber() : DraggableNumber(true) { - setEditableOnClick(true); + setEditableOnClick(true, true); } void mouseDown(MouseEvent const& e) override @@ -693,7 +694,6 @@ struct DraggableListNumber : public DraggableNumber { void paint(Graphics& g) override { if (hoveredDecimal >= 0) { - // TODO: make this colour Id configurable? g.setColour(outlineColour.withAlpha(isMouseButtonDown() ? 0.5f : 0.3f)); g.fillRoundedRectangle(hoveredDecimalPosition, 2.5f); } @@ -724,7 +724,6 @@ struct DraggableListNumber : public DraggableNumber { } if (hoveredDecimal >= 0) { - // TODO: make this colour Id configurable auto const highlightColour = outlineColour.withAlpha(isMouseButtonDown() ? 0.5f : 0.3f); nvgFillColor(nvg, NVGComponent::convertColour(highlightColour)); nvgFillRoundedRect(nvg, hoveredDecimalPosition.getX(), hoveredDecimalPosition.getY() - 1, hoveredDecimalPosition.getWidth(), hoveredDecimalPosition.getHeight(), 2.5f); @@ -759,6 +758,12 @@ struct DraggableListNumber : public DraggableNumber { repaint(); } } + + void textEditorReturnKeyPressed(TextEditor& editor) override + { + onReturnKey(0); + hideEditor(false); + } std::tuple getListItemAtPosition(int x, Rectangle* position = nullptr) { diff --git a/Source/Components/PropertiesPanel.h b/Source/Components/PropertiesPanel.h index cbe035236..1b297ac0b 100644 --- a/Source/Components/PropertiesPanel.h +++ b/Source/Components/PropertiesPanel.h @@ -322,22 +322,56 @@ class PropertiesPanel : public Component { struct FontComponent : public PropertiesPanelProperty { Value fontValue; StringArray options = Font::findAllTypefaceNames(); + bool isFontMissing = false; - FontComponent(String const& propertyName, Value& value) + FontComponent(String const& propertyName, Value& value, File const& extraFontsDir = File()) : PropertiesPanelProperty(propertyName) { - options.addIfNotAlreadyThere("Inter"); + StringArray extraFontOptions; + + if(extraFontsDir.isDirectory() && !extraFontsDir.isRoot()) { + auto patchFonts = Fonts::getFontsInFolder(extraFontsDir); + for (int n = 0; n < patchFonts.size(); n++) { + extraFontOptions.addIfNotAlreadyThere(patchFonts[n].getFileNameWithoutExtension()); + } + } +#if JUCE_WINDOWS + extraFontOptions.addIfNotAlreadyThere("Inter Regular"); +#else + extraFontOptions.addIfNotAlreadyThere("Inter"); +#endif + + auto offset = extraFontOptions.size(); + extraFontOptions.addArray(options); + + for (int n = 0; n < extraFontOptions.size(); n++) { + if (n == offset) + comboBox.getRootMenu()->addSeparator(); + + comboBox.getRootMenu()->addCustomItem(n + 1, std::make_unique(extraFontOptions[n]), nullptr, extraFontOptions[n]); - for (int n = 0; n < options.size(); n++) { - comboBox.getRootMenu()->addCustomItem(n + 1, std::make_unique(options[n]), nullptr, options[n]); } comboBox.setText(value.toString()); comboBox.getProperties().set("Style", "Inspector"); fontValue.referTo(value); - comboBox.onChange = [this]() { - fontValue.setValue(options[comboBox.getSelectedItemIndex()]); + comboBox.onChange = [this, extraFontOptions, propertyName]() { + auto fontName = extraFontOptions[comboBox.getSelectedItemIndex()]; + + if (fontName.isEmpty()) { + isFontMissing = true; + fontName = fontValue.toString(); + PropertiesPanelProperty::setName(propertyName + " (missing)"); + } + else { + isFontMissing = false; + PropertiesPanelProperty::setName(propertyName); + } + + lookAndFeelChanged(); + getParentComponent()->repaint(); + fontValue.setValue(fontName); }; setLookAndFeel(&LookAndFeel::getDefaultLookAndFeel()); @@ -354,12 +388,11 @@ class PropertiesPanel : public Component { void lookAndFeelChanged() override { - comboBox.setColour(ComboBox::textColourId, findColour(PlugDataColour::panelTextColourId)); + comboBox.setColour(ComboBox::textColourId, isFontMissing ? Colours::red : findColour(PlugDataColour::panelTextColourId)); } void setFont(String const& fontName) { - fontValue.setValue(fontValue); comboBox.setText(fontName); } diff --git a/Source/Components/WelcomePanel.h b/Source/Components/WelcomePanel.h index bb2a6b91f..888c92fd6 100644 --- a/Source/Components/WelcomePanel.h +++ b/Source/Components/WelcomePanel.h @@ -9,6 +9,7 @@ #include "Utility/CachedTextRender.h" #include "Utility/NanoVGGraphicsContext.h" #include "Components/BouncingViewport.h" +#include "Utility/PatchInfo.h" class WelcomePanel : public Component , public NVGComponent @@ -81,7 +82,7 @@ class WelcomePanel : public Component } }; - class NewOpenTile : public Component + class MainActionTile : public Component { NVGImage shadowImage; bool isHovered = false; @@ -91,11 +92,12 @@ class WelcomePanel : public Component enum TileType { New, - Open + Open, + Store }; TileType type; - NewOpenTile(TileType type) : type(type) + MainActionTile(TileType type) : type(type) { } @@ -120,13 +122,13 @@ class WelcomePanel : public Component auto lB = bounds.toFloat().expanded(0.5f); { - auto bgCol = !isHovered ? convertColour(findColour(PlugDataColour::canvasBackgroundColourId)) : convertColour(findColour(PlugDataColour::toolbarBackgroundColourId)); + auto bgCol = !isHovered ? convertColour(findColour(PlugDataColour::panelForegroundColourId)) : convertColour(findColour(PlugDataColour::toolbarBackgroundColourId)); // Draw border around nvgDrawRoundedRect(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), bgCol, convertColour(findColour(PlugDataColour::toolbarOutlineColourId)), Corners::largeCornerRadius); } - auto const bgColour = findColour(PlugDataColour::canvasBackgroundColourId); + auto const bgColour = findColour(PlugDataColour::panelForegroundColourId); auto const bgCol = convertColour(bgColour); auto const newOpenIconCol = convertColour(bgColour.contrasting().withAlpha(0.32f)); auto const iconSize = 48; @@ -175,6 +177,22 @@ class WelcomePanel : public Component nvgText(nvg, 92, 63, "Browse for a patch to open", NULL); break; } + case Store: { + nvgFontFace(nvg, "icon_font-Regular"); + nvgFillColor(nvg, bgCol); + nvgFontSize(nvg, 30); + nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); + nvgText(nvg, circleBounds.getCentreX(), circleBounds.getCentreY() - 4, Icons::Sparkle.toRawUTF8(), nullptr); + + nvgFontFace(nvg, "Inter-Bold"); + nvgFontSize(nvg, 12); + nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_LEFT); + nvgFillColor(nvg, NVGComponent::convertColour(findColour(PlugDataColour::panelTextColourId))); + nvgText(nvg, 92, 45, "Discover...", NULL); + + nvgFontFace(nvg, "Inter-Regular"); + nvgText(nvg, 92, 63, "Browse online patch store", NULL); + } default: break; } @@ -235,7 +253,28 @@ class WelcomePanel : public Component File patchFile; + enum TileType + { + Patch, + LibraryPatch + }; + + TileType tileType = Patch; + public: + WelcomePanelTile(WelcomePanel& welcomePanel, File& patchFile, String patchAuthor, float scale, bool favourited, Image const& thumbImage = Image()) + : isFavourited(favourited) + , parent(welcomePanel) + , snapshotScale(scale) + , thumbnailImageData(thumbImage) + , patchFile(patchFile) + { + tileName = patchFile.getFileNameWithoutExtension(); + tileSubtitle = patchAuthor; + tileType = LibraryPatch; + resized(); + } + WelcomePanelTile(WelcomePanel& welcomePanel, ValueTree subTree, String svgImage, Colour iconColour, float scale, bool favourited, Image const& thumbImage = Image()) : isFavourited(favourited) , parent(welcomePanel) @@ -244,7 +283,7 @@ class WelcomePanel : public Component { patchFile = File(subTree.getProperty("Path").toString()); tileName = patchFile.getFileNameWithoutExtension(); - + auto is24Hour = OSUtils::is24HourTimeFormat(); auto formatTimeDescription = [is24Hour](const Time& openTime, bool showDayAndDate = false) { @@ -271,9 +310,9 @@ class WelcomePanel : public Component }; - auto const accessedInPlugdasta = Time(static_cast(subTree.getProperty("Time"))); + auto const accessedInPlugdata = Time(static_cast(subTree.getProperty("Time"))); - tileSubtitle = formatTimeDescription(accessedInPlugdasta); + tileSubtitle = formatTimeDescription(accessedInPlugdata); auto const fileSize = patchFile.getSize(); @@ -291,9 +330,9 @@ class WelcomePanel : public Component // We need to show the time accessed from plugdata, which is saved in the settings XML // We want to show this again as well as in the subtile, but format it differently (with both Today/Yesterday and date) // because the popup menu may occlude the tile + subtitle - accessedTimeDescription = formatTimeDescription(accessedInPlugdasta, true); + accessedTimeDescription = formatTimeDescription(accessedInPlugdata, true); - updateGeneratedThumbnailIfNeeded(thumbImage, svgImage, iconColour); + updateGeneratedThumbnailIfNeeded(thumbImage, svgImage); } WelcomePanelTile(WelcomePanel& welcomePanel, String name, String subtitle, String svgImage, Colour iconColour, float scale, bool favourited, Image const& thumbImage = Image()) @@ -304,15 +343,15 @@ class WelcomePanel : public Component , tileSubtitle(subtitle) , thumbnailImageData(thumbImage) { - updateGeneratedThumbnailIfNeeded(thumbImage, svgImage, iconColour); + updateGeneratedThumbnailIfNeeded(thumbImage, svgImage); } - void updateGeneratedThumbnailIfNeeded(Image const& thumbnailImage, String& svgImage, Colour iconColour) - { + void updateGeneratedThumbnailIfNeeded(Image const& thumbnailImage, String& svgImage) { if (!thumbnailImage.isValid()) { + auto snapshotColour = LookAndFeel::getDefaultLookAndFeel().findColour(PlugDataColour::objectSelectedOutlineColourId).withAlpha(0.3f); snapshot = Drawable::createFromImageData(svgImage.toRawUTF8(), svgImage.getNumBytesAsUTF8()); if (snapshot) { - snapshot->replaceColour(Colours::black, iconColour); + snapshot->replaceColour(Colours::black, snapshotColour); } } @@ -331,27 +370,70 @@ class WelcomePanel : public Component PopupMenu tileMenu; - tileMenu.addItem(PlatformStrings::getBrowserTip(), [this]() { - if (patchFile.existsAsFile()) - patchFile.revealToUser(); - }); - tileMenu.addSeparator(); - tileMenu.addItem(isFavourited ? "Remove from favourites" : "Add to favourites", [this]() { - isFavourited = !isFavourited; - onFavourite(isFavourited); - }); - tileMenu.addSeparator(); - PopupMenu patchInfoSubMenu; - patchInfoSubMenu.addItem(String("Size: " + fileSizeDescription), false, false, nullptr); - patchInfoSubMenu.addSeparator(); - patchInfoSubMenu.addItem(String("Created: " + creationTimeDescription), false, false, nullptr); - patchInfoSubMenu.addItem(String("Modified: " + modifiedTimeDescription), false, false, nullptr); - patchInfoSubMenu.addItem(String("Accessed: " + accessedTimeDescription), false, false, nullptr); - tileMenu.addSubMenu(String(tileName + ".pd file info"), patchInfoSubMenu, true); - tileMenu.addSeparator(); - // TODO: we may want to be clearer about this - that it doesn't delete the file on disk - // Put this at he bottom, so it's not accidentally clicked on - tileMenu.addItem("Remove from recently opened", onRemove); + if (tileType == LibraryPatch) { + tileMenu.addItem(PlatformStrings::getBrowserTip(), [this]() { + if (patchFile.existsAsFile()) + patchFile.revealToUser(); + }); + + tileMenu.addSeparator(); + + auto metaFile = patchFile.getParentDirectory().getChildFile("meta.json"); + if(metaFile.existsAsFile()) { + + auto json = JSON::fromString(metaFile.loadFileAsString()); + auto patchInfo = PatchInfo(json); + + PopupMenu patchInfoSubMenu; + patchInfoSubMenu.addItem("Title: " + patchInfo.title, false, false, nullptr); + patchInfoSubMenu.addItem("Author: " + patchInfo.author, false, false, nullptr); + patchInfoSubMenu.addItem("Released: " + patchInfo.releaseDate, false, false, nullptr); + patchInfoSubMenu.addItem("About: " + patchInfo.description, false, false, nullptr); + + tileMenu.addSubMenu(String(tileName + " info"), patchInfoSubMenu, true); + } else { + tileMenu.addItem("Patch info not provided", false, false, nullptr); + } + + tileMenu.addSeparator(); + + // Put this at the bottom, so it's not accidentally clicked on + tileMenu.addItem("Delete from library...", [this]() { + Dialogs::showMultiChoiceDialog(&parent.confirmationDialog, parent.getParentComponent(), "Are you sure you want to delete: " + patchFile.getFileNameWithoutExtension(), [this](int choice) { + if (choice == 0) { + patchFile.getParentDirectory().deleteRecursively(true); + parent.triggerAsyncUpdate(); + } + }, { "Yes", "No" }, Icons::Warning); + }); + } else { + if (tileType == Patch) { + tileMenu.addItem(PlatformStrings::getBrowserTip(), [this]() { + if (patchFile.existsAsFile()) + patchFile.revealToUser(); + }); + + tileMenu.addSeparator(); + tileMenu.addItem(isFavourited ? "Remove from favourites" : "Add to favourites", [this]() { + isFavourited = !isFavourited; + onFavourite(isFavourited); + }); + + tileMenu.addSeparator(); + PopupMenu patchInfoSubMenu; + patchInfoSubMenu.addItem(String("Size: " + fileSizeDescription), false, false, nullptr); + patchInfoSubMenu.addSeparator(); + patchInfoSubMenu.addItem(String("Created: " + creationTimeDescription), false, false, nullptr); + patchInfoSubMenu.addItem(String("Modified: " + modifiedTimeDescription), false, false, nullptr); + patchInfoSubMenu.addItem(String("Accessed: " + accessedTimeDescription), false, false, nullptr); + tileMenu.addSubMenu(String(tileName + ".pd file info"), patchInfoSubMenu, true); + } + tileMenu.addSeparator(); + + // TODO: we may want to be clearer about this - that it doesn't delete the file on disk + // Put this at he bottom, so it's not accidentally clicked on + tileMenu.addItem("Remove from recently opened", onRemove); + } PopupMenu::Options options; options.withTargetComponent(this); @@ -409,7 +491,7 @@ class WelcomePanel : public Component }); } } else { - if (snapshot && !snapshotImage.isValid()) { + if (tileType != LibraryPatch && snapshot && !snapshotImage.isValid()) { snapshotImage = NVGImage(nvg, bounds.getWidth() * 2, (bounds.getHeight() - 32) * 2, [this](Graphics& g) { g.addTransform(AffineTransform::scale(2.0f)); snapshot->drawAt(g, 0, 0, 1.0f); @@ -423,15 +505,25 @@ class WelcomePanel : public Component auto lB = bounds.toFloat().expanded(0.5f); // Draw background even for images incase there is a transparent PNG - nvgDrawRoundedRect(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), convertColour(findColour(PlugDataColour::canvasBackgroundColourId)), convertColour(findColour(PlugDataColour::toolbarOutlineColourId)), Corners::largeCornerRadius); + nvgDrawRoundedRect(nvg, lB.getX(), lB.getY(), lB.getWidth(), lB.getHeight(), convertColour(findColour(PlugDataColour::panelForegroundColourId)), convertColour(findColour(PlugDataColour::toolbarOutlineColourId)), Corners::largeCornerRadius); if (thumbnailImageData.isValid()) { // Render the thumbnail image file that is in the root dir of the pd patch auto sB = bounds.toFloat().reduced(0.2f); snapshotImage.render(nvg, Rectangle(sB.getX() + 12, sB.getY(), sB.getWidth(), sB.getHeight() - 32)); - } else { + } else if (tileType == Patch) { // Otherwise render the generated snapshot snapshotImage.render(nvg, bounds.withTrimmedBottom(32)); + } else { + auto placeholderIconColour = LookAndFeel::getDefaultLookAndFeel().findColour(PlugDataColour::objectSelectedOutlineColourId).withAlpha(0.22f); + + // We draw the plugdata logo if library tiles don't have a thumbnail (patch snapshot is too busy) + nvgFillColor(nvg, NVGComponent::convertColour(placeholderIconColour)); + nvgFontFace(nvg, "icon_font-Regular"); + nvgFontSize(nvg, 68.0f); + nvgTextAlign(nvg, NVG_ALIGN_CENTER | NVG_ALIGN_MIDDLE); + nvgText(nvg, bounds.getCentreX(), (bounds.getHeight() - 30) * 0.5f, tileType == LibraryPatch ? Icons::PlugdataIconStandard.toRawUTF8() : Icons::Error.toRawUTF8(), nullptr); } + nvgRestore(nvg); // Draw border around @@ -561,12 +653,14 @@ class WelcomePanel : public Component setCachedComponentImage(new NVGSurface::InvalidationListener(editor->nvgSurface, this)); - newPatchTile = std::make_unique(NewOpenTile::New); - openPatchTile = std::make_unique(NewOpenTile::Open); + newPatchTile = std::make_unique(MainActionTile::New); + openPatchTile = std::make_unique(MainActionTile::Open); + storeTile = std::make_unique(MainActionTile::Store); newPatchTile->onClick = [this]() { editor->getTabComponent().newPatch(); }; openPatchTile->onClick = [this]() { editor->getTabComponent().openPatch(); }; - + storeTile->onClick = [this]() { Dialogs::showStore(editor); }; + triggerAsyncUpdate(); } @@ -590,6 +684,7 @@ class WelcomePanel : public Component searchQuery = newSearchQuery; if(newPatchTile) newPatchTile->setVisible(searchQuery.isEmpty()); if(openPatchTile) openPatchTile->setVisible(searchQuery.isEmpty()); + if(storeTile) storeTile->setVisible(searchQuery.isEmpty()); auto& tiles = currentTab == Home ? recentlyOpenedTiles : libraryTiles; for (auto* tile : tiles) { @@ -612,8 +707,7 @@ class WelcomePanel : public Component // Adjust the tile width to fit within the available width int actualTileWidth = (totalWidth - (numColumns - 1) * tileSpacing) / numColumns; - auto showNewOpenTiles = newPatchTile && openPatchTile && currentTab == Home && searchQuery.isEmpty(); - if (showNewOpenTiles) { + if (searchQuery.isEmpty()) { rowBounds = bounds.removeFromTop(100); // Position buttons centre if there are no recent items if (recentlyOpenedTiles.size() == 0) { @@ -627,8 +721,13 @@ class WelcomePanel : public Component auto const buttonY = getHeight() * 0.5f - 30; newPatchTile->setBounds(rowBounds.withX(startX).withWidth(buttonWidth).withY(buttonY)); openPatchTile->setBounds(rowBounds.withX(startX + buttonWidth + tileSpacing).withWidth(buttonWidth).withY(buttonY)); + + auto firstTileBounds = rowBounds.removeFromLeft(actualTileWidth * 1.5f); + storeTile->setBounds(firstTileBounds); } else { - newPatchTile->setBounds(rowBounds.removeFromLeft(actualTileWidth * 1.5f)); + auto firstTileBounds = rowBounds.removeFromLeft(actualTileWidth * 1.5f); + newPatchTile->setBounds(firstTileBounds); + storeTile->setBounds(firstTileBounds); rowBounds.removeFromLeft(4); openPatchTile->setBounds(rowBounds.withWidth(actualTileWidth * 1.5f + 4)); } @@ -641,7 +740,7 @@ class WelcomePanel : public Component int numRows = (tiles.size() + numColumns - 1) / numColumns; int totalHeight = (numRows * 160) + 200; - auto tilesBounds = Rectangle(24, showNewOpenTiles ? 146 : 24, totalWidth + 24, totalHeight + 24); + auto tilesBounds = Rectangle(24, searchQuery.isEmpty() ? 146 : 24, totalWidth + 24, totalHeight + 24); contentComponent.setBounds(tiles.size() ? tilesBounds : getLocalBounds()); @@ -680,6 +779,7 @@ class WelcomePanel : public Component { newPatchTile->setVisible(true); openPatchTile->setVisible(true); + storeTile->setVisible(false); for(auto* tile : recentlyOpenedTiles) { tile->setVisible(true); @@ -692,6 +792,7 @@ class WelcomePanel : public Component else { newPatchTile->setVisible(false); openPatchTile->setVisible(false); + storeTile->setVisible(true); for(auto* tile : recentlyOpenedTiles) { tile->setVisible(false); @@ -718,6 +819,9 @@ class WelcomePanel : public Component contentComponent.addAndMakeVisible(*newPatchTile); contentComponent.addAndMakeVisible(*openPatchTile); } + else { + contentComponent.addAndMakeVisible(*storeTile); + } if (recentlyOpenedTree.isValid()) { // Place favourited patches at the top @@ -725,6 +829,16 @@ class WelcomePanel : public Component auto subTree = recentlyOpenedTree.getChild(i); auto patchFile = File(subTree.getProperty("Path").toString()); + + if(!File(patchFile).existsAsFile()) + { + if(!subTree.hasProperty("Removable")) + { + recentlyOpenedTree.removeChild(subTree, nullptr); + } + continue; + } + auto patchThumbnailBase = File(patchFile.getParentDirectory().getFullPathName() + "\\" + patchFile.getFileNameWithoutExtension() + "_thumb"); auto favourited = subTree.hasProperty("Pinned") && static_cast(subTree.getProperty("Pinned")); @@ -747,7 +861,15 @@ class WelcomePanel : public Component } if (thumbImage.isNull()) { if (patchFile.existsAsFile()) { - silhoutteSvg = OfflineObjectRenderer::patchToSVG(patchFile.loadFileAsString()); + auto cachedSilhouette = patchSvgCache.find(patchFile.getFullPathName()); + if(cachedSilhouette != patchSvgCache.end()) + { + silhoutteSvg = cachedSilhouette->second; + } + else { + silhoutteSvg = OfflineObjectRenderer::patchToSVG(patchFile.loadFileAsString()); + patchSvgCache[patchFile.getFullPathName()] = silhoutteSvg; + } } } @@ -796,30 +918,27 @@ class WelcomePanel : public Component libraryTiles.clear(); auto addTile = [this](File& patchFile){ - auto patchThumbnailBase = File(patchFile.getParentDirectory().getFullPathName() + "\\" + patchFile.getFileNameWithoutExtension() + "_thumb"); - StringArray possibleExtensions { ".png", ".jpg", ".jpeg", ".gif" }; + auto pName = patchFile.getFileNameWithoutExtension(); + auto foundThumbs = patchFile.getParentDirectory().findChildFiles(File::findFiles, true, pName + "_thumb.png;" + pName + "_thumb.jpg;" + pName + "_thumb.jpeg;" + pName + "_thumb.gif"); float scale = 1.0f; Image thumbImage; - for (auto& ext : possibleExtensions) { - auto patchThumbnail = patchThumbnailBase.withFileExtension(ext); - if (patchThumbnail.existsAsFile()) { - FileInputStream fileStream(patchThumbnail); - if (fileStream.openedOk()) { - thumbImage = ImageFileFormat::loadFrom(fileStream).convertedToFormat(Image::ARGB); + for (auto& thumb: foundThumbs) { + FileInputStream fileStream(thumb); + if (fileStream.openedOk()) { + thumbImage = ImageFileFormat::loadFrom(fileStream).convertedToFormat(Image::ARGB); + if (thumbImage.isValid()) break; - } } } - String placeholderIcon; - if(!thumbImage.isValid()) + auto metaFile = patchFile.getParentDirectory().getChildFile("meta.json"); + String author; + if(metaFile.existsAsFile()) { - scale = 0.6f; - placeholderIcon = libraryPlaceholderIcon; + auto json = JSON::fromString(metaFile.loadFileAsString()); + author = json["Author"].toString(); } - auto snapshotColour = LookAndFeel::getDefaultLookAndFeel().findColour(PlugDataColour::objectSelectedOutlineColourId).withAlpha(0.3f); - - auto* tile = libraryTiles.add(new WelcomePanelTile(*this, patchFile.getFileNameWithoutExtension(), "", placeholderIcon, snapshotColour, scale, false, thumbImage)); + auto* tile = libraryTiles.add(new WelcomePanelTile(*this, patchFile, author, scale, false, thumbImage)); tile->onClick = [this, patchFile]() mutable { if (patchFile.existsAsFile()) { editor->pd->autosave->checkForMoreRecentAutosave(patchFile, editor, [this, patchFile]() { @@ -888,22 +1007,8 @@ class WelcomePanel : public Component { triggerAsyncUpdate(); } - - static inline String const libraryPlaceholderIcon = "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n" - "\n"; - - std::unique_ptr newPatchTile, openPatchTile; + + std::unique_ptr newPatchTile, openPatchTile, storeTile; ContentComponent contentComponent = ContentComponent(*this); BouncingViewport viewport; @@ -916,6 +1021,9 @@ class WelcomePanel : public Component String searchQuery; Tab currentTab = Home; + UnorderedMap patchSvgCache; + + std::unique_ptr confirmationDialog; // To make the library panel update automatically class LibraryFSListener : public FileSystemWatcher::Listener diff --git a/Source/Constants.h b/Source/Constants.h index e0918bb2e..dbd4a19e6 100644 --- a/Source/Constants.h +++ b/Source/Constants.h @@ -104,10 +104,14 @@ struct Icons { inline static String const Duplicate = "2"; inline static String const Cut = "3"; + inline static String const Storage = CharPointer_UTF8 ("\xc3\x90"); + inline static String const Money = CharPointer_UTF8 ("\xc3\x91"); + inline static String const Time = CharPointer_UTF8 ("\xc3\x92"); + inline static String const Store = CharPointer_UTF8 ("\xc3\x8f"); inline static String const PanelExpand = CharPointer_UTF8("\xc3\x8d"); inline static String const PanelContract = CharPointer_UTF8("\xc3\x8c"); inline static String const ItemGrid = " "; - + inline static String const AlignLeft = "4"; inline static String const AlignRight = "5"; inline static String const AlignHCentre = "6"; @@ -250,6 +254,11 @@ struct Icons { inline static String const GlyphDuck = CharPointer_UTF8("\xc6\xa0"); inline static String const GlyphBallance = CharPointer_UTF8("\xc6\xa7"); inline static String const GlyphPan = CharPointer_UTF8("\xc6\xa8"); + + // plugdata icon with three styles + inline static String const PlugdataIconStandard = CharPointer_UTF8("\xc2\xbc"); + inline static String const PlugdataIconFilled = CharPointer_UTF8("\xc2\xbd"); + inline static String const PlugdataIconSilhouette = CharPointer_UTF8("\xc2\xbe"); }; enum PlugDataColour { @@ -441,4 +450,4 @@ namespace PlatformStrings { return "Reveal in file browser"; #endif } -} \ No newline at end of file +} diff --git a/Source/Dialogs/AboutPanel.h b/Source/Dialogs/AboutPanel.h index 85b090074..42e262ff8 100644 --- a/Source/Dialogs/AboutPanel.h +++ b/Source/Dialogs/AboutPanel.h @@ -45,8 +45,12 @@ class AboutPanel : public Component { { "tomara-x", "Documentation, testing" } }; - StringArray const sponsors = { + StringArray const corporateSponsors = { "Deskew Technologies", + "Jet Brains" + }; + + StringArray const sponsors = { "Naskomusic", "epsil0ndelta", "polarity", @@ -94,7 +98,7 @@ class AboutPanel : public Component { Path firstShadowPath; firstShadowPath.addRoundedRectangle(Rectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), contributors.size() * 48).reduced(4), Corners::largeCornerRadius); - StackShadow::renderDropShadow(hash("credits_panel"), g, firstShadowPath, Colour(0, 0, 0).withAlpha(0.32f), 8); + StackShadow::renderDropShadow(hash("contributors"), g, firstShadowPath, Colour(0, 0, 0).withAlpha(0.32f), 8); for (int i = 0; i < contributors.size(); i++) { auto rowBounds = bounds.removeFromTop(48); @@ -116,13 +120,41 @@ class AboutPanel : public Component { bounds.removeFromTop(24); - Fonts::drawStyledText(g, "Sponsors", bounds.getX(), bounds.getY() - 8, bounds.getWidth(), 15.0f, findColour(PlugDataColour::panelTextColourId), Semibold, 15.0f); + Fonts::drawStyledText(g, "Corporate sponsors", bounds.getX(), bounds.getY() - 8, bounds.getWidth(), 15.0f, findColour(PlugDataColour::panelTextColourId), Semibold, 15.0f); bounds.removeFromTop(16); Path secondShadowPath; - secondShadowPath.addRoundedRectangle(Rectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), sponsors.size() * 32).reduced(4), Corners::largeCornerRadius); - StackShadow::renderDropShadow(hash("credits_panel"), g, secondShadowPath, Colour(0, 0, 0).withAlpha(0.32f), 8); + secondShadowPath.addRoundedRectangle(Rectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), corporateSponsors.size() * 32).reduced(4), Corners::largeCornerRadius); + StackShadow::renderDropShadow(hash("corporate_credits_panel"), g, secondShadowPath, Colour(0, 0, 0).withAlpha(0.32f), 8); + for (int i = 0; i < corporateSponsors.size(); i++) { + auto rowBounds = bounds.removeFromTop(36); + auto first = i == 0; + auto last = i == (corporateSponsors.size() - 1); + auto& name = corporateSponsors[i]; + Path outline; + outline.addRoundedRectangle(rowBounds.getX(), rowBounds.getY(), rowBounds.getWidth(), rowBounds.getHeight(), Corners::largeCornerRadius, Corners::largeCornerRadius, first, first, last, last); + + g.setColour(findColour(PlugDataColour::panelForegroundColourId)); + g.fillPath(outline); + + g.setColour(findColour(PlugDataColour::outlineColourId)); + g.strokePath(outline, PathStrokeType(1)); + + Fonts::drawText(g, name, rowBounds.reduced(12, 2), findColour(PlugDataColour::panelTextColourId), 15); + + jassert(!bounds.isEmpty()); + } + + bounds.removeFromTop(24); + + Fonts::drawStyledText(g, "Sponsors", bounds.getX(), bounds.getY() - 8, bounds.getWidth(), 15.0f, findColour(PlugDataColour::panelTextColourId), Semibold, 15.0f); + + bounds.removeFromTop(16); + + Path thirdShadowPath; + thirdShadowPath.addRoundedRectangle(Rectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), sponsors.size() * 32).reduced(4), Corners::largeCornerRadius); + StackShadow::renderDropShadow(hash("credits_panel"), g, thirdShadowPath, Colour(0, 0, 0).withAlpha(0.32f), 8); for (int i = 0; i < sponsors.size(); i++) { auto rowBounds = bounds.removeFromTop(36); auto first = i == 0; @@ -145,7 +177,7 @@ class AboutPanel : public Component { int getDesiredHeight() { - return sponsors.size() * 36; + return (sponsors.size() + corporateSponsors.size() + 1) * 36; } }; @@ -170,14 +202,14 @@ class AboutPanel : public Component { license.setMultiLine(true); license.setText(licenseText); license.setFont(Font(15)); - license.setLineSpacing(1.1f); + license.setLineSpacing(1.0f); addAndMakeVisible(license); } } void resized() override { - license.setBounds(getLocalBounds().withTrimmedTop(32).reduced(16, 8)); + license.setBounds(getLocalBounds().withTrimmedTop(32).reduced(16, 4)); } void paint(Graphics& g) override diff --git a/Source/Dialogs/Dialogs.cpp b/Source/Dialogs/Dialogs.cpp index 8aab69cd1..f4d483e40 100644 --- a/Source/Dialogs/Dialogs.cpp +++ b/Source/Dialogs/Dialogs.cpp @@ -37,6 +37,7 @@ #include "Connection.h" #include "Deken.h" #include "Standalone/PlugDataWindow.h" +#include "PatchStore.h" Dialog::Dialog(std::unique_ptr* ownerPtr, Component* editor, int childWidth, int childHeight, bool showCloseButton, int margin) : height(childHeight) @@ -217,6 +218,10 @@ void Dialogs::showMainMenu(PluginEditor* editor, Component* centre) Dialogs::showDeken(editor); break; } + case MainMenu::MenuItem::Discover: { + Dialogs::showStore(editor); + break; + } case MainMenu::MenuItem::Settings: { Dialogs::showSettingsDialog(editor); break; @@ -363,6 +368,15 @@ void Dialogs::showDeken(PluginEditor* editor) editor->openedDialog.reset(dialog); } +void Dialogs::showStore(PluginEditor* editor) +{ + auto* dialog = new Dialog(&editor->openedDialog, editor, 850, 550, true); + auto* dialogContent = new PatchStore(); + dialog->setViewedComponent(dialogContent); + editor->openedDialog.reset(dialog); +} + + StringArray DekenInterface::getExternalPaths() { StringArray searchPaths; diff --git a/Source/Dialogs/Dialogs.h b/Source/Dialogs/Dialogs.h index 84854973c..067ca6a8a 100644 --- a/Source/Dialogs/Dialogs.h +++ b/Source/Dialogs/Dialogs.h @@ -86,7 +86,7 @@ class Dialog : public Component { } if (closeButton) { - auto closeButtonBounds = Rectangle(viewedComponent->getRight() - 35, viewedComponent->getY() + 8, 28, 28); + auto closeButtonBounds = Rectangle(viewedComponent->getRight() - 35, viewedComponent->getY() + 6, 28, 28); closeButton->setBounds(closeButtonBounds); } } @@ -171,6 +171,7 @@ struct Dialogs { static void showObjectMenu(PluginEditor* parent, Component* target); static void showDeken(PluginEditor* editor); + static void showStore(PluginEditor* editor); static void dismissFileDialog(); diff --git a/Source/Dialogs/MainMenu.h b/Source/Dialogs/MainMenu.h index c811fd8d3..dd52f2ded 100644 --- a/Source/Dialogs/MainMenu.h +++ b/Source/Dialogs/MainMenu.h @@ -97,7 +97,7 @@ class MainMenu : public PopupMenu { addCustomItem(getMenuItemID(MenuItem::FindExternals), std::unique_ptr(menuItems[getMenuItemIndex(MenuItem::FindExternals)]), nullptr, "Find externals..."); - // addCustomItem(getMenuItemID(MenuItem::Discover), std::unique_ptr(menuItems[getMenuItemIndex(MenuItem::Discover)]), nullptr, "Discover..."); + addCustomItem(getMenuItemID(MenuItem::Discover), std::unique_ptr(menuItems[getMenuItemIndex(MenuItem::Discover)]), nullptr, "Discover..."); addCustomItem(getMenuItemID(MenuItem::Settings), std::unique_ptr(menuItems[getMenuItemIndex(MenuItem::Settings)]), nullptr, "Settings..."); addCustomItem(getMenuItemID(MenuItem::About), std::unique_ptr(menuItems[getMenuItemIndex(MenuItem::About)]), nullptr, "About..."); @@ -280,6 +280,7 @@ class MainMenu : public PopupMenu { CompiledMode, Compile, FindExternals, + Discover, Settings, About }; @@ -297,7 +298,7 @@ class MainMenu : public PopupMenu { return item - 1; } - StackArray menuItems = { + StackArray menuItems = { new IconMenuItem(Icons::New, "New patch", false, false), new IconMenuItem(Icons::Open, "Open patch...", false, false), new IconMenuItem(Icons::History, "Recently opened", true, false), @@ -311,6 +312,7 @@ class MainMenu : public PopupMenu { new IconMenuItem(Icons::DevTools, "Compile...", false, false), new IconMenuItem(Icons::Externals, "Find externals...", false, false), + new IconMenuItem(Icons::Sparkle, "Discover...", false, false), new IconMenuItem(Icons::Settings, "Settings...", false, false), new IconMenuItem(Icons::Info, "About...", false, false), }; diff --git a/Source/Dialogs/PatchStore.h b/Source/Dialogs/PatchStore.h new file mode 100644 index 000000000..aa59e6310 --- /dev/null +++ b/Source/Dialogs/PatchStore.h @@ -0,0 +1,1170 @@ +#include +#include "Utility/PatchInfo.h" + +#define STB_IMAGE_RESIZE_IMPLEMENTATION +#include "Utility/stb_image_resize.h" + + +class DownloadPool : public DeletedAtShutdown +{ +public: + struct DownloadListener + { + virtual void downloadProgressed(hash32 hash, float progress) {}; + virtual void databaseDownloadCompleted(HeapArray> const& patches) {}; + virtual void patchDownloadCompleted(hash32 hash, bool success) {}; + virtual void imageDownloadCompleted(hash32 hash, Image const& downloadedImage) {}; + }; + + ~DownloadPool() + { + imagePool.removeAllJobs(true, -1); + patchPool.removeAllJobs(true, -1); + clearSingletonInstance(); + } + + void addDownloadListener(DownloadListener* listener) + { + listeners.insert(listener); + } + + void removeDownloadListener(DownloadListener* listener) + { + listeners.erase(listener); + } + + void cancelImageDownloads() + { + imagePool.removeAllJobs(true, 500); + } + + void downloadDatabase() + { + imagePool.addJob([this](){ + SmallArray patches; + std::unique_ptr webstream; + + webstream = std::make_unique(URL("https://plugdata.org/store.json"), false); + webstream->connect(nullptr); + + if (webstream->isError() || webstream->getStatusCode() == 400) { + // TODO: show error + return; + } + + MemoryBlock block; + webstream->readIntoMemoryBlock(block); + MemoryInputStream memstream(block, false); + + auto parsedData = JSON::parse(memstream); + auto patchData = parsedData["Patches"]; + if (patchData.isArray()) { + for (int i = 0; i < patchData.size(); ++i) { + var const& patchObject = patchData[i]; + patches.add(PatchInfo(patchObject)); + } + } + + HeapArray> sortedPatches; + for(auto& patch : patches) + { + sortedPatches.emplace_back(patch, patch.isPatchInstalled() + (2 * patch.updateAvailable())); + } + + std::sort(sortedPatches.begin(), sortedPatches.end(), [](std::pair const& first, std::pair const& second) { + auto& [patchA, flagsA] = first; + auto& [patchB, flagsB] = second; + + if(flagsA > flagsB) return true; + if(flagsA < flagsB) return false; + + return patchA.releaseDate > patchB.releaseDate; + }); + + MessageManager::callAsync([this, sortedPatches]() { + for(auto& listener : listeners) + { + listener->databaseDownloadCompleted(sortedPatches); + } + }); + }); + } + + void downloadImage(hash32 hash, URL location) + { + imagePool.addJob([this, hash, location](){ + auto updateImageListeners = [this](hash32 hash, Image& image) { + MessageManager::callAsync([this, hash, image]() { + for(auto& listener : listeners) + { + listener->imageDownloadCompleted(hash, image); + } + }); + }; + + static UnorderedMap downloadImageCache; + static CriticalSection cacheMutex; + + { + ScopedLock lock(cacheMutex); + + if (downloadImageCache.contains(hash)) { + if (auto img = downloadImageCache[hash]; img.isValid()) { + updateImageListeners(hash, img); + return; + } + } + } + + MemoryBlock block; + // Load the image data from the URL + WebInputStream memstream(location, false); + memstream.readIntoMemoryBlock(block); + auto image = ImageFileFormat::loadFrom(block.getData(), block.getSize()); + { + ScopedLock lock(cacheMutex); + downloadImageCache[hash] = image; + } + updateImageListeners(hash, image); + }); + } + + void downloadPatch(hash32 downloadHash, PatchInfo const& info) + { + patchPool.addJob([this, downloadHash, info](){ + MemoryBlock data; + + int statusCode = 0; + + auto instream = URL(info.download).createInputStream(URL::InputStreamOptions(URL::ParameterHandling::inAddress) + .withConnectionTimeoutMs(10000).withStatusCode(&statusCode)); + int64 totalBytes = instream->getTotalLength(); + int64 bytesDownloaded = 0; + + MemoryOutputStream mo(data, true); + + while (true) { + auto written = mo.writeFromInputStream(*instream, 8192); + + if (written == 0) + break; + + bytesDownloaded += written; + + float progress = static_cast(bytesDownloaded) / static_cast(totalBytes); + + if(cancelledDownloads.contains(downloadHash)) { + cancelledDownloads.erase(downloadHash); + MessageManager::callAsync([this, downloadHash]() mutable { + for(auto& listener : listeners) + { + listener->patchDownloadCompleted(downloadHash, false); + } + }); + return; + } + + MessageManager::callAsync([this, downloadHash, progress]() mutable { + for(auto& listener : listeners) + { + listener->downloadProgressed(downloadHash, progress); + } + }); + } + + MemoryInputStream input(data, false); + ZipFile zip(input); + + auto patchesDir = ProjectInfo::appDataDir.getChildFile("Patches"); + auto result = zip.uncompressTo(patchesDir, true); + auto downloadedPatch = patchesDir.getChildFile(zip.getEntry(0)->filename); + + auto targetLocation = downloadedPatch.getParentDirectory().getChildFile(info.getNameInPatchFolder()); + targetLocation.deleteRecursively(true); + + downloadedPatch.moveFileTo(targetLocation); + + auto metaFile = targetLocation.getChildFile("meta.json"); + if(!metaFile.existsAsFile()) + { + metaFile.replaceWithText(info.json); + } + + auto macOSTrash = ProjectInfo::appDataDir.getChildFile("Patches").getChildFile("__MACOSX"); + if(macOSTrash.isDirectory()) + { + macOSTrash.deleteRecursively(); + } + + MessageManager::callAsync([this, downloadHash, result](){ + for(auto& listener : listeners) + { + listener->patchDownloadCompleted(downloadHash, result.wasOk()); + } + }); + + }); + } + + void cancelDownload(hash32 hash) + { + cancelledDownloads.insert(hash); + } + +private: + + UnorderedSet listeners; + + CriticalSection cancelledDownloadsLock; + UnorderedSet cancelledDownloads; + + ThreadPool imagePool = ThreadPool(3); + ThreadPool patchPool = ThreadPool(2); + +public: + JUCE_DECLARE_SINGLETON(DownloadPool, false); +}; + +JUCE_IMPLEMENT_SINGLETON(DownloadPool); + +class OnlineImage : public Component, public DownloadPool::DownloadListener +{ +public: + OnlineImage(bool roundedTop, bool roundedBottom) + : roundTop(roundedTop) + , roundBottom(roundedBottom) + { + spinner.setSize(50, 50); + spinner.setCentrePosition(getWidth() / 2, getHeight() / 2); + addAndMakeVisible(spinner); + setInterceptsMouseClicks(false, false); + DownloadPool::getInstance()->addDownloadListener(this); + } + + ~OnlineImage() override + { + DownloadPool::getInstance()->removeDownloadListener(this); + } + + void imageDownloadCompleted(hash32 hash, Image const& image) override { + if(hash == imageHash) + { + scaledImage = resampleImageToFit(image); + repaint(); + spinner.stopSpinning(); + } + }; + + + void setImageURL(const URL& url) + { + imageHash = hash(url.toString(false)); + scaledImage = Image(); + + // Lock the thread to safely update the image URL + imageURL = url; + DownloadPool::getInstance()->downloadImage(imageHash, url); + spinner.startSpinning(); + + repaint(); + } + + void paint(Graphics& g) override + { + // Create a rounded rectangle to use as a mask + Path roundedRectanglePath; + roundedRectanglePath.addRoundedRectangle(0, 0, getWidth(), getHeight(), Corners::largeCornerRadius, Corners::largeCornerRadius, roundTop, roundTop, roundBottom, roundBottom); + + if (!scaledImage.isValid()) { + g.setColour(findColour(PlugDataColour::panelForegroundColourId)); + g.fillPath(roundedRectanglePath); + return; + } + + g.saveState(); + + g.reduceClipRegion(roundedRectanglePath); + + Rectangle targetBounds(0, 0, static_cast(getWidth()), static_cast(getHeight())); + + g.setImageResamplingQuality(Graphics::highResamplingQuality); + g.drawImage(scaledImage, targetBounds, RectanglePlacement::centred | RectanglePlacement::fillDestination); + + g.restoreState(); + } + + Image resampleImageToFit(Image const& downloadedImage) + { + Image result; +#if JUCE_MAC + if (downloadedImage.isValid()) + { + auto srcWidth = downloadedImage.getWidth(); + auto srcHeight = downloadedImage.getHeight(); + + int targetWidth = getWidth(); + int targetHeight = getHeight(); + + // Calculate the aspect ratios + float srcAspect = static_cast(srcWidth) / static_cast(srcHeight); + float targetAspect = static_cast(targetWidth) / static_cast(targetHeight); + + // Crop the image to match the target aspect ratio + int cropX = 0, cropY = 0, cropWidth = srcWidth, cropHeight = srcHeight; + + if (srcAspect > targetAspect) { + // Image is wider than the target, crop width + cropWidth = static_cast(srcHeight * targetAspect); + cropX = (srcWidth - cropWidth) / 2; // Center the crop + } else if (srcAspect < targetAspect) { + // Image is taller than the target, crop height + cropHeight = static_cast(srcWidth / targetAspect); + cropY = (srcHeight - cropHeight) / 2; // Center the crop + } + + // Resample the image to the new size + result = downloadedImage.getClippedImage(Rectangle(cropX, cropY, cropWidth, cropHeight)); + } +#else + if (!downloadedImage.isValid()) + return result; + + auto srcWidth = downloadedImage.getWidth(); + auto srcHeight = downloadedImage.getHeight(); + + int targetWidth = getWidth() * scale; + int targetHeight = getHeight() * scale; + + // Calculate the aspect ratios + float srcAspect = static_cast(srcWidth) / static_cast(srcHeight); + float targetAspect = static_cast(targetWidth) / static_cast(targetHeight); + + // Crop the image to match the target aspect ratio + int cropX = 0, cropY = 0, cropWidth = srcWidth, cropHeight = srcHeight; + + if (srcAspect > targetAspect) { + // Image is wider than the target, crop width + cropWidth = static_cast(srcHeight * targetAspect); + cropX = (srcWidth - cropWidth) / 2; // Center the crop + } else if (srcAspect < targetAspect) { + // Image is taller than the target, crop height + cropHeight = static_cast(srcWidth / targetAspect); + cropY = (srcHeight - cropHeight) / 2; // Center the crop + } + + int numChannels = 0; + auto srcData = packImageData(downloadedImage.getClippedImage(Rectangle(cropX, cropY, cropWidth, cropHeight)), numChannels); + + HeapArray resampledData(targetWidth * targetHeight * numChannels); + + // Perform resampling +//#define CUSTOM_FILTER +#ifdef CUSTOM_FILTER + stbir_resize_uint8_generic( + srcData.data(), cropWidth, cropHeight, 0, // Source image + resampledData.data(), targetWidth, targetHeight, 0, // Destination image + numChannels, // Number of channels + numChannels == 4, + 1, + STBIR_EDGE_CLAMP, + STBIR_FILTER_DEFAULT, + STBIR_COLORSPACE_SRGB, + NULL + ); +#else + stbir_resize_uint8( + srcData.data(), cropWidth, cropHeight, 0, // Source image + resampledData.data(), targetWidth, targetHeight, 0, // Destination image + numChannels // Number of channels + ); +#endif + + result = Image(downloadedImage.getFormat(), targetWidth, targetHeight, true); + Image::BitmapData destData(result, Image::BitmapData::writeOnly); + + for (int y = 0; y < targetHeight; ++y) + { + unsigned char* destRow = destData.getLinePointer(y); + std::memcpy(destRow, &resampledData[y * targetWidth * numChannels], targetWidth * numChannels); + } +#endif + + return result; + } + + static HeapArray packImageData(const Image &image, int &numChannels) + { + if (!image.isValid()) + return {}; + + Image::BitmapData bitmapData(image, Image::BitmapData::readOnly); + + // Determine the number of channels + switch (image.getFormat()) { + case Image::PixelFormat::RGB: + numChannels = 3; + break; + case Image::PixelFormat::ARGB: + numChannels = 4; + break; + case Image::PixelFormat::SingleChannel: + numChannels = 1; + break; + default: + return {}; + } + + // Allocate tightly packed buffer + int width = image.getWidth(); + int height = image.getHeight(); + HeapArray packedData(width * height * numChannels); + + for (int y = 0; y < height; ++y) { + const unsigned char *row = bitmapData.getLinePointer(y); + + for (int x = 0; x < width; ++x) { + int srcIndex = x * bitmapData.pixelStride; + int destIndex = (y * width + x) * numChannels; + + // Pack based on format + switch (image.getFormat()) { + case Image::ARGB: { + packedData[destIndex + 0] = row[srcIndex + 1]; // Red + packedData[destIndex + 1] = row[srcIndex + 2]; // Green + packedData[destIndex + 2] = row[srcIndex + 3]; // Blue + if (numChannels == 4) + packedData[destIndex + 0] = row[srcIndex + 3]; // Alpha + } + break; + case Image::RGB: { + packedData[destIndex + 0] = row[srcIndex + 0]; // Red + packedData[destIndex + 1] = row[srcIndex + 1]; // Green + packedData[destIndex + 2] = row[srcIndex + 2]; // Blue + } + break; + case Image::SingleChannel: { + packedData[destIndex] = row[srcIndex]; + } + break; + default: + break; + + } + } + } + + return packedData; + } + + void resized() override + { + spinner.setCentrePosition(getWidth() / 2, getHeight() / 2); + } + + static void setScreenScale(float scaleFactor) + { + scale = scaleFactor; + } + +private: + bool roundTop, roundBottom; + URL imageURL; + hash32 imageHash; + Image scaledImage; + Spinner spinner; + + static inline float scale = 0.0f; + + JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(OnlineImage) +}; + + +class PatchDisplay : public Component { +public: + PatchDisplay(PatchInfo const& patchInfo, std::function clickCallback, int statusFlag) + : image(true, false) + , callback(clickCallback) + , info(patchInfo) + , isInstalled(statusFlag >= 1) + , needsUpdate(statusFlag >= 2) + { + image.setImageURL("https://plugdata.org/thumbnails/png/" + patchInfo.thumbnailUrl + ".png"); + addAndMakeVisible(image); + } + + bool matchesQuery(String const& query) + { + return query.isEmpty() || info.title.containsIgnoreCase(query) || info.author.containsIgnoreCase(query) || info.description.containsIgnoreCase(query); + } + +private: + void paintOverChildren(Graphics& g) override + { + auto b = getLocalBounds().reduced(6); + g.setColour(findColour(PlugDataColour::toolbarOutlineColourId)); + g.drawRoundedRectangle(b.toFloat(), Corners::largeCornerRadius, 1.0f); + } + + void paint(Graphics& g) override + { + auto b = getLocalBounds().reduced(6); + + Path p; + p.addRoundedRectangle(b.reduced(3.0f), Corners::largeCornerRadius); + StackShadow::renderDropShadow(hash("patch_display"), g, p, Colour(0, 0, 0).withAlpha(0.4f), 7, { 0, 1 }); + + if (isMouseOver()) { + g.setColour(findColour(PlugDataColour::panelActiveBackgroundColourId)); + } else { + g.setColour(findColour(PlugDataColour::panelForegroundColourId)); + } + + g.fillRoundedRectangle(b.toFloat(), Corners::largeCornerRadius); + + g.setColour(findColour(PlugDataColour::toolbarOutlineColourId)); + g.drawRoundedRectangle(b.toFloat(), Corners::largeCornerRadius, 1.0f); + + b.removeFromTop(171); // space for image + auto nameArea = b.removeFromTop(24); + + g.setColour(findColour(PlugDataColour::panelActiveBackgroundColourId).withAlpha(0.5f)); + g.fillRect(nameArea); + + auto platformArea = nameArea.removeFromRight(80).reduced(4).toFloat(); + platformArea.removeFromLeft(10); + + auto textColour = findColour(PlugDataColour::panelTextColourId); + Fonts::drawText(g, "by " + info.author, nameArea.withTrimmedLeft(10), textColour, 13.5f, Justification::left); + + auto textBounds = b.reduced(10, 4); + + Fonts::drawStyledText(g, info.title, textBounds.removeFromTop(30), textColour, Bold, 15, Justification::left); + + auto layout = TextLayout(); + auto description = AttributedString(info.description); + description.setFont(Font(14.5f)); + description.setColour(textColour); + layout.createLayout(description, textBounds.getWidth(), 150); + + layout.draw(g, textBounds.withTrimmedBottom(32).toFloat()); + + auto bottomRow = b.removeFromBottom(32).reduced(11); + Fonts::drawStyledText(g, info.price, bottomRow, textColour, Semibold, 15, Justification::centredLeft); + + if(needsUpdate){ + Fonts::drawStyledText(g, "Update available", bottomRow, textColour, Semibold, 15, Justification::centredRight); + } + if(isInstalled) { + Fonts::drawStyledText(g, "Installed", bottomRow, textColour, Semibold, 15, Justification::centredRight); + } + } + + void resized() override + { + auto b = getLocalBounds().reduced(6); + image.setBounds(b.removeFromTop(171).withWidth(248)); + } + + void mouseEnter(MouseEvent const& e) override + { + repaint(); + } + + void mouseExit(MouseEvent const& e) override + { + repaint(); + } + + void mouseDown(MouseEvent const& e) override + { + callback(info); + } + + OnlineImage image; + + std::function callback; + PatchInfo info; + bool isInstalled; + bool needsUpdate; +}; + +class PatchContainer : public Component +{ + int const displayWidth = 260; + int const displayHeight = 315; + + OwnedArray patchDisplays; + HeapArray> patches; + +public: + + std::function patchClicked; + + void filterPatches(String query) + { + for (auto* patch : patchDisplays) { + if (patch->matchesQuery(query)) { + patch->setVisible(true); + } else { + patch->setVisible(false); + } + } + + resized(); + } + + void showPatches(HeapArray> const& patchesToShow) + { + patches = patchesToShow; + + patchDisplays.clear(); + + for (auto& patch : patches) { + auto* display = patchDisplays.add(new PatchDisplay(patch.first, patchClicked, patch.second)); + addAndMakeVisible(display); + } + + setSize(getWidth(), ((patches.size() / (getWidth() / displayWidth)) * (displayHeight + 8)) + 12); + resized(); // Even if size if the same, we still want to call resize + } + + void resized() override + { + auto bounds = getLocalBounds().reduced(6); + + auto currentRow = bounds.removeFromTop(displayHeight); + + auto numColumns = currentRow.getWidth() / displayWidth; + auto extraSpace = currentRow.getWidth() - (numColumns * displayWidth); + auto padding = (extraSpace / numColumns) / 2; + + for (auto* display : patchDisplays) { + if (!display->isVisible()) + continue; + + if (currentRow.getWidth() < displayWidth) { + bounds.removeFromTop(8); + currentRow = bounds.removeFromTop(displayHeight); + } + + currentRow.removeFromLeft(padding); + display->setBounds(currentRow.removeFromLeft(displayWidth)); + currentRow.removeFromLeft(padding); + } + } + + HeapArray> getPatches() + { + return patches; + } +}; + +class PatchFullDisplay : public Component, public DownloadPool::DownloadListener +{ + PatchInfo currentPatch; + hash32 patchHash; + File const patchesDir = ProjectInfo::appDataDir.getChildFile("Patches"); + PatchContainer morePatches; + + class Viewport : public BouncingViewport + { + void paint(Graphics& g) override + { + g.fillAll(findColour(PlugDataColour::panelForegroundColourId)); + } + }; + Viewport viewport; + + class LinkButton : public TextButton { + public: + enum Type + { + AlreadyInstalled, + Download, + Store, + View, + Cancel + }; + Type type; + + LinkButton(Type type) : type(type) + { + setClickingTogglesState(true); + setConnectedEdges(12); + } + + String getIcon() + { + if(type == AlreadyInstalled) return isMouseOver() ? Icons::Reset : Icons::Checkmark; + if(type == Download) return Icons::Download; + if(type == Store) return Icons::Store; + if(type == View) return Icons::Info; + if(type == Cancel) return {}; + return {}; + } + + String getText() + { + if(type == AlreadyInstalled) return isMouseOver() ? "Reinstall" : "Installed"; + if(type == Download) return "Download"; + if(type == Store) return "View in store"; + if(type == View) return "View online"; + if(type == Cancel) return "Cancel"; + return {}; + } + + void setType(Type newType) + { + type = newType; + repaint(); + } + + void paint(Graphics& g) override + { + auto b = getLocalBounds().reduced(2.0f, 4.0f).toFloat(); + + auto mouseOver = isMouseOver(); + auto fillColour = findColour(PlugDataColour::toolbarActiveColourId); + auto outlineColour = fillColour; + auto greyColour = findColour(PlugDataColour::panelActiveBackgroundColourId); + + if (type == Cancel) { + fillColour = greyColour; + outlineColour = greyColour; + } else if (type == View) { + fillColour = mouseOver ? greyColour.contrasting(0.6f) : findColour(PlugDataColour::panelBackgroundColourId).withAlpha(0.0f); + outlineColour = greyColour.contrasting(0.6f); + } + + auto textColour = fillColour.contrasting(0.96f); + + if (mouseOver) { + fillColour = fillColour.brighter(0.4f); + outlineColour = outlineColour.brighter(0.4f); + } + + g.setColour(fillColour); + g.fillRoundedRectangle(b, Corners::defaultCornerRadius); + + g.setColour(outlineColour); + g.drawRoundedRectangle(b, Corners::defaultCornerRadius, 1.5f); + + auto boldFont = Fonts::getBoldFont().withHeight(14.0f); + auto iconFont = Fonts::getIconFont().withHeight(14.0f); + + // Draw icon and text + AttributedString attrStr; + attrStr.setJustification(Justification::centred); + attrStr.append(getIcon(), iconFont, textColour); + attrStr.append(" " + getText(), boldFont, textColour); + attrStr.draw(g, b); + } + }; + +public: + PatchFullDisplay() + : image(true, true) + { + setVisible(true); + addAndMakeVisible(viewButton); + addAndMakeVisible(downloadButton); + addAndMakeVisible(image); + addAndMakeVisible(morePatches); + + morePatches.patchClicked = [this](PatchInfo const& patch) + { + showPatch(patch, patches); + }; + + setSize(840, 1084); + + viewport.setScrollBarsShown(true, false); + viewport.setViewedComponent(this, false); + + downloadButton.onClick = [this]() { + if(downloadProgress == 0) + { + repaint(); + + if (currentPatch.isPatchArchive()) { + downloadProgress = 1; + DownloadPool::getInstance()->downloadPatch(patchHash, currentPatch); + } + else { + URL(currentPatch.download).launchInDefaultBrowser(); + } + } + else { + DownloadPool::getInstance()->cancelDownload(patchHash); + } + }; + + viewButton.onClick = [this]() { + URL("https://plugdata.org/store-item.html?id=" + currentPatch.title).launchInDefaultBrowser(); + }; + + DownloadPool::getInstance()->addDownloadListener(this); + } + + ~PatchFullDisplay() + { + DownloadPool::getInstance()->removeDownloadListener(this); + } + + void downloadProgressed(hash32 downloadHash, float progress) override { + if(downloadHash == patchHash) { + downloadButton.setType(LinkButton::Cancel); + downloadProgress = std::max(progress * 100, 1); + repaint(); + } + }; + + void patchDownloadCompleted(hash32 downloadHash, bool success) override { + if(downloadHash == patchHash) { + downloadProgress = 0; + auto* parent = viewport.getParentComponent(); + if(success) { + Dialogs::showMultiChoiceDialog(&confirmationDialog, parent, "Successfully installed " + currentPatch.title, [](int){}, { "Dismiss" }, Icons::Checkmark); + } + else { + Dialogs::showMultiChoiceDialog(&confirmationDialog, parent, "Failed to install " + currentPatch.title, [](int){}, { "Dismiss" }); + } + + downloadButton.setType(success ? LinkButton::AlreadyInstalled : LinkButton::Download); + } + }; + + PatchFullDisplay::Viewport& getViewport() + { + return viewport; + } + + void showPatch(PatchInfo const& patchInfo, HeapArray> const& allPatches) { + downloadProgress = 0; + patchHash = hash(patchInfo.title); + patches = allPatches; + currentPatch = patchInfo; + + auto fileName = URL(currentPatch.download).getFileName(); + + if (currentPatch.isPatchInstalled()) { + downloadButton.setType(LinkButton::AlreadyInstalled); + } else if (currentPatch.isPatchArchive()) { + downloadButton.setType(LinkButton::Download); + } else { + downloadButton.setType(LinkButton::Store); + } + + image.setImageURL("https://plugdata.org/thumbnails/png/" + patchInfo.thumbnailUrl + ".png"); + viewport.setVisible(true); + + morePatches.showPatches(filterPatches(patchInfo, allPatches)); + } + + HeapArray> filterPatches(PatchInfo const& targetPatch, HeapArray> toFilter) + { + std::shuffle(std::begin(toFilter), std::end(toFilter), std::random_device()); + + HeapArray> result; + for(auto& [patch, flags] : toFilter) + { + if(result.size() >= 3) break; + if(flags == 0 && targetPatch.author == patch.author && patch.title != targetPatch.title) + { + result.add({patch, 0}); + } + } + + if(result.size() <= 1) + { + result.clear(); + + for(auto& [patch, flags] : toFilter) + { + if(result.size() >= 3) break; + if(flags == 0 && patch.title != targetPatch.title) result.add({patch, 0}); + } + } + + return result; + } + + void paintOverChildren(Graphics& g) override + { + // Drag image outline + g.setColour(findColour(PlugDataColour::toolbarOutlineColourId)); + g.drawRoundedRectangle(image.getBounds().toFloat(), Corners::largeCornerRadius, 1.0f); + + if(downloadProgress != 0.0f) + { + g.setFont(Fonts::getCurrentFont().withHeight(14.0f)); + g.setColour(findColour(PlugDataColour::panelTextColourId).withAlpha(0.75f)); + g.drawText("Installing: " + String(downloadProgress) + "%", downloadButton.getBounds().translated(0, 30), Justification::centred); + + auto bounds = downloadButton.getBounds().reduced(2, 4); + + g.saveState(); + Path clipPath; + clipPath.addRoundedRectangle(bounds, Corners::defaultCornerRadius); + g.reduceClipRegion(clipPath); + + g.setColour(findColour(PlugDataColour::toolbarActiveColourId)); + g.fillRect(bounds.removeFromBottom(4).withWidth(bounds.getWidth() * (downloadProgress / 100.0f))); + g.restoreState(); + } + } + + + void paint(Graphics& g) override + { + auto b = getLocalBounds().reduced(12); + + g.fillAll(findColour(PlugDataColour::panelForegroundColourId)); + + auto contentArea = b.reduced(20, 6); + auto textColour = findColour(PlugDataColour::panelTextColourId); + g.setColour(textColour); + + g.setFont(Fonts::getBoldFont().withHeight(26)); + g.drawText(currentPatch.title, contentArea.removeFromTop(40), Justification::centredLeft); + + g.setFont(Fonts::getCurrentFont().withHeight(16.5f)); + g.drawText("by " + currentPatch.author, contentArea.removeFromTop(24), Justification::centredLeft); + + contentArea.removeFromTop(8); + + // Separator Line + g.setColour(textColour.withAlpha(0.25f)); + g.drawHorizontalLine(contentArea.getY(), contentArea.getX(), contentArea.getRight()); + + contentArea.removeFromTop(8); + + auto layout = TextLayout(); + AttributedString descriptionText(currentPatch.description); + descriptionText.setFont(Font(15.5f)); + descriptionText.setColour(textColour); + layout.createLayout(descriptionText, contentArea.getWidth(), contentArea.getHeight()); + + layout.draw(g, contentArea.removeFromTop(30).translated(0, 4).toFloat()); + + auto extraInfoBounds = contentArea.removeFromTop(72).reduced(0, 12).translated(0, -4); + Path p; + p.addRoundedRectangle(extraInfoBounds, Corners::largeCornerRadius); + StackShadow::renderDropShadow(hash("patch_extra_info"), g, p, Colour(0, 0, 0).withAlpha(0.1f), 7, { 0, 1 }); + + g.setColour(findColour(PlugDataColour::panelForegroundColourId)); + g.fillPath(p); // Adjust the thickness as needed + + g.setColour(findColour(PlugDataColour::outlineColourId)); + g.strokePath(p, PathStrokeType(0.5f)); // Adjust the thickness as needed + + auto hasSizeInfo = currentPatch.size.isNotEmpty(); + int extraInfoItemWidth = getWidth() / (hasSizeInfo ? 3 : 2); + auto drawExtraInfo = [this, extraInfoItemWidth, &extraInfoBounds](Graphics& g, String const& icon, String const& label, String const& value) mutable { + auto infoBounds = extraInfoBounds.removeFromLeft(extraInfoItemWidth).withSizeKeepingCentre(110, 32).translated(-12, 0); + + g.setColour(findColour(PlugDataColour::panelTextColourId)); + g.setFont(Fonts::getIconFont().withHeight(15)); + g.drawText(icon, infoBounds.removeFromLeft(24), Justification::centredLeft); + + g.setFont(Fonts::getBoldFont().withHeight(15)); + g.drawText(label, infoBounds.removeFromTop(16), Justification::centredLeft); + + g.setFont(Fonts::getDefaultFont().withHeight(15)); + g.drawText(value, infoBounds, Justification::centredLeft); + }; + + if(hasSizeInfo) + { + drawExtraInfo(g, Icons::Storage, "Size", currentPatch.size); + } + drawExtraInfo(g, Icons::Money, "Price", currentPatch.price); + drawExtraInfo(g, Icons::Time, "Release date", currentPatch.releaseDate); + + auto imageBounds = contentArea.removeFromTop(500).withSizeKeepingCentre(getWidth(), 500); + g.setColour(findColour(PlugDataColour::panelBackgroundColourId)); + g.fillRect(imageBounds); + + contentArea.removeFromTop(12); + + g.setColour(textColour); + g.setFont(Fonts::getSemiBoldFont().withHeight(16.5f)); + g.drawText("More patches", contentArea.removeFromTop(30), Justification::centredLeft); + + g.setColour(textColour.withAlpha(0.25f)); + g.drawHorizontalLine(contentArea.getY(), contentArea.getX(), contentArea.getRight()); + } + + void resized() override + { + auto b = getLocalBounds().reduced(20, 6); + auto buttonBounds = b.removeFromTop(204).removeFromTop(40).translated(-11, 12); + + image.setBounds(b.removeFromTop(500).withSizeKeepingCentre(460, 450)); + + b.removeFromTop(40); + + morePatches.setBounds(b.withHeight(morePatches.getHeight())); + + downloadButton.setBounds(buttonBounds.removeFromRight(110)); + buttonBounds.removeFromRight(14); + viewButton.setBounds(buttonBounds.removeFromRight(110)); + } + + OnlineImage image; + LinkButton downloadButton = LinkButton(LinkButton::Download); + LinkButton viewButton = LinkButton(LinkButton::View); + + int downloadProgress = 0; + std::unique_ptr confirmationDialog; + HeapArray> patches; +}; + +struct PatchStore : public Component, public DownloadPool::DownloadListener { + + PatchContainer patchContainer; + BouncingViewport contentViewport; + PatchFullDisplay patchFullDisplay; + + MainToolbarButton backButton = MainToolbarButton(Icons::Back); + MainToolbarButton refreshButton = MainToolbarButton(Icons::Refresh); + MainToolbarButton searchButton = MainToolbarButton(Icons::Search); + + SearchEditor input; + Spinner spinner; + +public: + PatchStore() + { + contentViewport.setViewedComponent(&patchContainer, false); + patchContainer.setVisible(true); + addAndMakeVisible(contentViewport); + + contentViewport.setScrollBarsShown(true, false, true, false); + + spinner.startSpinning(); + DownloadPool::getInstance()->downloadDatabase(); + + addChildComponent(patchFullDisplay.getViewport()); + + patchContainer.patchClicked = [this](PatchInfo const& patch) { + patchFullDisplay.showPatch(patch, patchContainer.getPatches()); + backButton.setVisible(true); + refreshButton.setVisible(false); + input.setVisible(false); + input.setText("", sendNotification); + searchButton.setVisible(false); + }; + + searchButton.setClickingTogglesState(true); + searchButton.onClick = [this](){ + if (searchButton.getToggleState()) { + input.setVisible(true); + input.grabKeyboardFocus(); + input.setText(""); + + } else { + input.setVisible(false); + } + }; + addAndMakeVisible(searchButton); + + addChildComponent(backButton); + + backButton.onClick = [this]() { + patchFullDisplay.getViewport().setVisible(false); + backButton.setVisible(false); + refreshButton.setVisible(true); + input.setVisible(false); + input.setText("", sendNotification); + searchButton.setVisible(true); + }; + + backButton.setColour(TextButton::buttonColourId, Colours::transparentBlack); + backButton.setColour(TextButton::buttonOnColourId, Colours::transparentBlack); + + refreshButton.setTooltip("Refresh packages"); + refreshButton.setEnabled(false); + addAndMakeVisible(refreshButton); + refreshButton.onClick = [this]() { + DownloadPool::getInstance()->cancelImageDownloads(); + spinner.startSpinning(); + DownloadPool::getInstance()->downloadDatabase(); + refreshButton.setEnabled(false); + }; + + input.setTextToShowWhenEmpty("Type to search for patches", findColour(PlugDataColour::panelTextColourId).withAlpha(0.5f)); + input.setColour(TextEditor::textColourId, findColour(PlugDataColour::panelTextColourId)); + input.setBorder({ 1, 3, 5, 1 }); + input.setJustification(Justification::centredLeft); + input.onTextChange = [this]() { + patchContainer.filterPatches(input.getText()); + }; + input.onFocusLost = [this]() { + if (searchButton.isMouseOver()) { + return; + } + + if (input.getText().isEmpty()) { + searchButton.setToggleState(false, dontSendNotification); + input.setVisible(false); + } + }; + addChildComponent(input); + addChildComponent(spinner); + DownloadPool::getInstance()->addDownloadListener(this); + } + + ~PatchStore() + { + DownloadPool::getInstance()->removeDownloadListener(this); + DownloadPool::getInstance()->cancelImageDownloads(); + } + + void paint(Graphics& g) override + { + OnlineImage::setScreenScale(g.getInternalContext().getPhysicalPixelScaleFactor()); + + g.setColour(findColour(PlugDataColour::panelBackgroundColourId)); + g.fillRoundedRectangle(getLocalBounds().toFloat(), Corners::windowCornerRadius); + + auto bounds = getLocalBounds().removeFromTop(40).toFloat(); + + Path p; + p.addRoundedRectangle(bounds.getX(), bounds.getY(), bounds.getWidth(), bounds.getHeight(), Corners::largeCornerRadius, Corners::largeCornerRadius, true, true, false, false); + + g.setColour(findColour(PlugDataColour::toolbarBackgroundColourId)); + g.fillPath(p); + + Fonts::drawStyledText(g, "Discover", Rectangle(0.0f, 4.0f, getWidth(), 32.0f), findColour(PlugDataColour::panelTextColourId), Semibold, 15, Justification::centred); + + g.setColour(findColour(PlugDataColour::toolbarOutlineColourId)); + g.drawLine(0, 40, getWidth(), 40); + } + + void resized() override + { + input.setBounds(getLocalBounds().removeFromTop(40).reduced(48, 5).withTrimmedRight(40)); + + auto b = getLocalBounds().withTrimmedTop(40); + + patchFullDisplay.getViewport().setBounds(b); + + contentViewport.setBounds(b); + + patchContainer.setSize(getWidth(), patchContainer.getHeight()); + + backButton.setBounds(2, 0, 40, 40); + refreshButton.setBounds(2, 0, 40, 40); + searchButton.setBounds(getWidth() - 82, 0, 40, 40); + + spinner.setSize(50, 50); + spinner.setCentrePosition(b.getWidth() / 2, b.getHeight() / 2); + } + + void databaseDownloadCompleted(HeapArray> const& patches) override { + patchContainer.showPatches(patches); + refreshButton.setEnabled(true); + spinner.stopSpinning(); + } +}; diff --git a/Source/Heavy/DPFExporter.h b/Source/Heavy/DPFExporter.h index 499daa857..721cb5589 100644 --- a/Source/Heavy/DPFExporter.h +++ b/Source/Heavy/DPFExporter.h @@ -278,20 +278,29 @@ class DPFExporter : public ExporterBase { outputFile.getChildFile("bin").getChildFile(name + ".lv2").copyDirectoryTo(outputFile.getChildFile(name + ".lv2")); if (vst3) outputFile.getChildFile("bin").getChildFile(name + ".vst3").copyDirectoryTo(outputFile.getChildFile(name + ".vst3")); -#if JUCE_WINDOWS if (vst2) +#if JUCE_WINDOWS outputFile.getChildFile("bin").getChildFile(name + "-vst.dll").moveFileTo(outputFile.getChildFile(name + "-vst.dll")); #elif JUCE_LINUX - if (vst2) outputFile.getChildFile("bin").getChildFile(name + "-vst.so").moveFileTo(outputFile.getChildFile(name + "-vst.so")); #elif JUCE_MAC - if (vst2) outputFile.getChildFile("bin").getChildFile(name + ".vst").copyDirectoryTo(outputFile.getChildFile(name + ".vst")); #endif if (clap) outputFile.getChildFile("bin").getChildFile(name + ".clap").moveFileTo(outputFile.getChildFile(name + ".clap")); - if (jack) + if (jack) { +#if JUCE_MAC + if (exportType == 2) { + outputFile.getChildFile("bin").getChildFile(name + ".app").moveFileTo(outputFile.getChildFile(name + ".app")); + } else { + outputFile.getChildFile("bin").getChildFile(name).moveFileTo(outputFile.getChildFile(name)); + } +#elif JUCE_WINDOWS + outputFile.getChildFile("bin").getChildFile(name + ".exe").moveFileTo(outputFile.getChildFile(name + ".exe")); +#else outputFile.getChildFile("bin").getChildFile(name).moveFileTo(outputFile.getChildFile(name)); +#endif + } bool compilationExitCode = getExitCode(); diff --git a/Source/Iolet.cpp b/Source/Iolet.cpp index 8f4afab4f..eba55c7e0 100644 --- a/Source/Iolet.cpp +++ b/Source/Iolet.cpp @@ -95,7 +95,7 @@ void Iolet::render(NVGcontext* nvg) bool Iolet::hitTest(int x, int y) { // If locked, don't intercept mouse clicks - if (locked || commandLocked) + if (locked) return false; if (patchDownwardsOnly && isInlet && !cnv->connectingWithDrag) @@ -122,7 +122,7 @@ bool Iolet::hitTest(int x, int y) void Iolet::mouseDrag(MouseEvent const& e) { // Ignore when locked or if middlemouseclick? - if (locked || e.mods.isMiddleButtonDown() || (patchDownwardsOnly && isInlet)) + if (locked || commandLocked || e.mods.isMiddleButtonDown() || (patchDownwardsOnly && isInlet)) return; if (!cnv->connectionCancelled && cnv->connectionsBeingCreated.empty() && e.getLengthOfMousePress() > 100) { @@ -165,7 +165,7 @@ void Iolet::mouseDrag(MouseEvent const& e) void Iolet::mouseUp(MouseEvent const& e) { - if (locked || e.mods.isRightButtonDown()) + if (locked || commandLocked || e.mods.isRightButtonDown()) return; bool wasDragged = e.mouseWasDraggedSinceMouseDown(); diff --git a/Source/Objects/FloatAtomObject.h b/Source/Objects/FloatAtomObject.h index 152e66fb5..1bdc22c6c 100644 --- a/Source/Objects/FloatAtomObject.h +++ b/Source/Objects/FloatAtomObject.h @@ -45,6 +45,7 @@ class FloatAtomObject final : public ObjectBase { addMouseListener(this, true); + input.setEditableOnClick(false, true); input.dragStart = [this]() { startEdition(); }; diff --git a/Source/Objects/ListObject.h b/Source/Objects/ListObject.h index 11625a0d7..bbd7ead95 100644 --- a/Source/Objects/ListObject.h +++ b/Source/Objects/ListObject.h @@ -5,7 +5,7 @@ */ class ListObject final : public ObjectBase - , public KeyListener { +{ AtomHelper atomHelper; DraggableListNumber listLabel; @@ -39,11 +39,15 @@ class ListObject final : public ObjectBase object->updateBounds(); } }; + + listLabel.onReturnKey = [this](double) + { + updateFromGui(true); + }; listLabel.onEditorShow = [this]() { startEdition(); auto* editor = listLabel.getCurrentTextEditor(); - editor->addKeyListener(this); editor->setColour(TextEditor::focusedOutlineColourId, Colours::transparentBlack); editor->setBorder({ 0, 1, 3, 0 }); editorActive = true; @@ -112,7 +116,7 @@ class ListObject final : public ObjectBase void updateFromGui(bool force = false) { - auto text = listLabel.getText(); + auto text = listLabel.getText(true); if (force || text != getListText()) { SmallArray list = pd::Atom::atomsFromString(text); setList(list); @@ -187,7 +191,7 @@ class ListObject final : public ObjectBase repaint(); } - bool keyPressed(KeyPress const& key, Component*) override + bool keyPressed(KeyPress const& key) override { if (key.getKeyCode() == KeyPress::returnKey) { updateFromGui(true); diff --git a/Source/Objects/NoteObject.h b/Source/Objects/NoteObject.h index b572d5ee6..22ebf20b1 100644 --- a/Source/Objects/NoteObject.h +++ b/Source/Objects/NoteObject.h @@ -4,6 +4,8 @@ // WARRANTIES, see the file, "LICENSE.txt," in this distribution. */ +#include "Utility/Fonts.h" + class NoteObject final : public ObjectBase { Colour textColour; @@ -425,6 +427,10 @@ class NoteObject final : public ObjectBase { if (typefaceName.isEmpty() || typefaceName == "Inter") { return Fonts::getVariableFont().withStyle(style).withHeight(fontHeight); } + + // Check if there is a patch font loaded via the patch loading + if (auto patchFont = Fonts::findFont(cnv->patch.getCurrentFile(), typefaceName); patchFont.has_value()) + return (*patchFont).withStyle(style).withHeight(fontHeight); return { typefaceName, static_cast(fontHeight), style }; } diff --git a/Source/Objects/NumberObject.h b/Source/Objects/NumberObject.h index 2bf38d4c3..bb19b73df 100644 --- a/Source/Objects/NumberObject.h +++ b/Source/Objects/NumberObject.h @@ -60,6 +60,7 @@ class NumberObject final : public ObjectBase { input.setColour(Label::textColourId, convertColour(foregroundCol)); }; + input.setEditableOnClick(false, true); input.onEditorHide = [this]() { stopEdition(); }; diff --git a/Source/Pd/Library.cpp b/Source/Pd/Library.cpp index 693fd7b86..d26ada38a 100644 --- a/Source/Pd/Library.cpp +++ b/Source/Pd/Library.cpp @@ -345,11 +345,8 @@ File Library::findFile(String const& fileToFind) auto pathTree = SettingsFile::getInstance()->getValueTree().getChildWithName("Paths"); for (auto path : pathTree) { auto searchPath = File(path.getProperty("Path").toString()); - if (!searchPath.exists() || !searchPath.isDirectory()) - continue; - auto childFile = searchPath.getChildFile(fileToFind); - if (childFile.existsAsFile()) + if (OSUtils::isFileFast(childFile.getFullPathName())) return childFile; } diff --git a/Source/PluginMode.h b/Source/PluginMode.h index 6375451bd..33b02ec30 100644 --- a/Source/PluginMode.h +++ b/Source/PluginMode.h @@ -21,6 +21,8 @@ class PluginMode : public Component , desktopWindow(editor->getPeer()) , windowBounds(editor->getBounds().withPosition(editor->getTopLevelComponent()->getPosition())) { + editor->pd->initialiseIntoPluginmode = false; + if (ProjectInfo::isStandalone) { // If the window is already maximised, unmaximise it to prevent problems #if JUCE_LINUX || JUCE_BSD diff --git a/Source/Sidebar/Inspector.h b/Source/Sidebar/Inspector.h index f05ee174a..62cba05f6 100644 --- a/Source/Sidebar/Inspector.h +++ b/Source/Sidebar/Inspector.h @@ -116,8 +116,9 @@ class Inspector : public Component { panel.setContentWidth(getWidth() - 16); } - static PropertiesPanelProperty* createPanel(int type, String const& name, Value* value, StringArray& options, std::function onInteractionFn = nullptr) + PropertiesPanelProperty* createPanel(int type, String const& name, Value* value, StringArray& options, std::function onInteractionFn = nullptr) { + switch (type) { case tString: return new PropertiesPanel::EditableComponent(name, *value); @@ -135,8 +136,14 @@ class Inspector : public Component { return new PropertiesPanel::RangeComponent(name, *value, false); case tRangeInt: return new PropertiesPanel::RangeComponent(name, *value, true); - case tFont: + case tFont: { + if (auto* editor = findParentComponentOfClass()) { + if(auto* cnv = editor->getCurrentCanvas()) { + return new PropertiesPanel::FontComponent(name, *value, cnv->patch.getCurrentFile().getParentDirectory()); + } + } return new PropertiesPanel::FontComponent(name, *value); + } default: return new PropertiesPanel::EditableComponent(name, *value); } diff --git a/Source/TabComponent.cpp b/Source/TabComponent.cpp index d0825430a..f3fbf8aba 100644 --- a/Source/TabComponent.cpp +++ b/Source/TabComponent.cpp @@ -62,7 +62,7 @@ Canvas* TabComponent::openPatch(const URL& path) } } } - + auto patch = pd->loadPatch(path); return openPatch(patch, true); } @@ -112,8 +112,7 @@ Canvas* TabComponent::openPatch(pd::Patch::Ptr existingPatch, bool warnIfAlready cnv->restoreViewportState(); triggerAsyncUpdate(); - - sendTabUpdateToVisibleCanvases(); + tabVisibilityMessageUpdater.triggerAsyncUpdate(); static bool alreadyOpeningInNewWindow = false; if (canvases.size() > 1 && !alreadyOpeningInNewWindow && ProjectInfo::isStandalone && SettingsFile::getInstance()->getProperty("open_patches_in_window")) { @@ -542,7 +541,7 @@ void TabComponent::showTab(Canvas* cnv, int splitIndex) editor->nvgSurface.invalidateAll(); - sendTabUpdateToVisibleCanvases(); + tabVisibilityMessageUpdater.triggerAsyncUpdate(); editor->sidebar->hideParameters(); editor->sidebar->clearSearchOutliner(); @@ -551,6 +550,11 @@ void TabComponent::showTab(Canvas* cnv, int splitIndex) addLastShownTab(cnv, splitIndex); } +void TabComponent::TabVisibilityMessageUpdater::handleAsyncUpdate() +{ + parent->sendTabUpdateToVisibleCanvases(); +} + Canvas* TabComponent::getCurrentCanvas() { if (editor->pluginMode) { diff --git a/Source/TabComponent.h b/Source/TabComponent.h index e4ea85223..d948bf3bd 100644 --- a/Source/TabComponent.h +++ b/Source/TabComponent.h @@ -334,4 +334,14 @@ class TabComponent : public Component PluginEditor* editor; PluginProcessor* pd; + + struct TabVisibilityMessageUpdater : public AsyncUpdater + { + TabVisibilityMessageUpdater(TabComponent* parent) : parent(parent) {}; + + void handleAsyncUpdate() override; + TabComponent* parent; + }; + + TabVisibilityMessageUpdater tabVisibilityMessageUpdater = TabVisibilityMessageUpdater(this); }; diff --git a/Source/Utility/Fonts.h b/Source/Utility/Fonts.h index ba990b865..055aa7865 100644 --- a/Source/Utility/Fonts.h +++ b/Source/Utility/Fonts.h @@ -63,6 +63,40 @@ struct Fonts { static Font setCurrentFont(Font const& font) { return instance->currentTypeface = font.getTypefacePtr(); } + static Array getFontsInFolder(File const& patchFile) + { + return patchFile.getParentDirectory().findChildFiles(File::findFiles, true, "*.ttf;*.otf;"); + } + + static std::optional findFont(File const& dirToSearch, String const& typefaceFileName) + { + Array fontFiles = dirToSearch.getParentDirectory().findChildFiles(File::findFiles, true, "*.ttf;*.otf;"); + + for (auto font : fontFiles) { + if(font.getFileNameWithoutExtension() == typefaceFileName) + { + auto it = fontTable.find(font.getFullPathName()); + if(it != fontTable.end()) + { + return it->second; + } + else if (font.existsAsFile()) + { + auto fileStream = font.createInputStream(); + if (fileStream == nullptr) break; + + MemoryBlock fontData; + fileStream->readIntoMemoryBlock(fontData); + auto typeface = Typeface::createSystemTypefaceFor(fontData.getData(), fontData.getSize()); + fontTable[font.getFullPathName()] = typeface; + return typeface; + } + } + } + + return std::nullopt; + } + // For drawing icons with icon font static void drawIcon(Graphics& g, String const& icon, Rectangle bounds, Colour colour, int fontHeight = -1, bool centred = true) { @@ -187,4 +221,6 @@ struct Fonts { Typeface::Ptr monoTypeface; Typeface::Ptr variableTypeface; Typeface::Ptr tabularTypeface; + + static inline UnorderedMap fontTable = UnorderedMap(); }; diff --git a/Source/Utility/OSUtils.cpp b/Source/Utility/OSUtils.cpp index ba2c6bd1b..dde841405 100644 --- a/Source/Utility/OSUtils.cpp +++ b/Source/Utility/OSUtils.cpp @@ -307,6 +307,11 @@ bool OSUtils::isDirectoryFast(juce::String const& path) return fs::is_directory(path.toStdString()); } +bool OSUtils::isFileFast(juce::String const& path) +{ + return fs::is_regular_file(path.toStdString()); +} + hash32 OSUtils::getUniqueFileHash(juce::String const& path) { return hash(fs::canonical(path.toStdString()).c_str()); diff --git a/Source/Utility/OSUtils.h b/Source/Utility/OSUtils.h index b67286ead..8c047eae1 100644 --- a/Source/Utility/OSUtils.h +++ b/Source/Utility/OSUtils.h @@ -40,6 +40,7 @@ struct OSUtils { static SmallArray iterateDirectory(juce::File const& directory, bool recursive, bool onlyFiles, int maximum = -1); static bool isDirectoryFast(juce::String const& path); + static bool isFileFast(juce::String const& path); static hash32 getUniqueFileHash(juce::String const& path); static KeyboardLayout getKeyboardLayout(); diff --git a/Source/Utility/ObjectThemeManager.h b/Source/Utility/ObjectThemeManager.h index d7a77241a..5b0babff7 100644 --- a/Source/Utility/ObjectThemeManager.h +++ b/Source/Utility/ObjectThemeManager.h @@ -30,7 +30,7 @@ class ObjectThemeManager { auto& lnf = LookAndFeel::getDefaultLookAndFeel(); bg = lnf.findColour(PlugDataColour::guiObjectBackgroundColourId); fg = lnf.findColour(PlugDataColour::canvasTextColourId); - lbl = lnf.findColour(PlugDataColour::toolbarTextColourId); + lbl = lnf.findColour(PlugDataColour::commentTextColourId); ln = lnf.findColour(PlugDataColour::guiObjectInternalOutlineColour); } diff --git a/Source/Utility/PatchInfo.h b/Source/Utility/PatchInfo.h new file mode 100644 index 000000000..9f0c524cf --- /dev/null +++ b/Source/Utility/PatchInfo.h @@ -0,0 +1,80 @@ +// +// Created by alexw on 5/12/2024. +// + +#pragma once + +class PatchInfo { +public: + String title; + String author; + String releaseDate; + String download; + String description; + String price; + String thumbnailUrl; + String size; + String json; + String version; + + PatchInfo() = default; + + PatchInfo(var const& jsonData) + { + title = jsonData["Title"]; + author = jsonData["Author"]; + releaseDate = jsonData["Release date"]; + download = jsonData["Download"]; + description = jsonData["Description"]; + price = jsonData["Price"]; + thumbnailUrl = jsonData["StoreThumb"]; + version = jsonData["Version"]; + json = JSON::toString(jsonData, false); + } + + bool isPatchArchive() const + { + auto fileName = URL(download).getFileName(); + return fileName.endsWith(".zip") || fileName.endsWith(".plugdata"); + } + + String getNameInPatchFolder() const + { + return title.toLowerCase().replace(" ", "-") + "-" + String::toHexString(hash(author)); + } + + bool isPatchInstalled() const + { + auto patchesFolder = ProjectInfo::appDataDir.getChildFile("Patches"); + + for (auto &file: OSUtils::iterateDirectory(patchesFolder, false, false)) { + if (OSUtils::isDirectoryFast(file.getFullPathName())) { + auto patchFileName = getNameInPatchFolder(); + if (file.getFileName() == patchFileName) { + return true; + } + } + } + return false; + } + + bool updateAvailable() const + { + auto patchesFolder = ProjectInfo::appDataDir.getChildFile("Patches"); + + for (auto &file: OSUtils::iterateDirectory(patchesFolder, false, false)) { + if (OSUtils::isDirectoryFast(file.getFullPathName())) { + auto patchFileName = getNameInPatchFolder(); + + if (file.getFileName() == patchFileName) { + auto metaFile = file.getChildFile("meta.json"); + if(metaFile.existsAsFile()) + { + return JSON::parse(metaFile)["Version"].toString() != version; + } + } + } + } + return false; + } +}; diff --git a/Source/Utility/SettingsFile.cpp b/Source/Utility/SettingsFile.cpp index a21a1bac0..1e5642cdb 100644 --- a/Source/Utility/SettingsFile.cpp +++ b/Source/Utility/SettingsFile.cpp @@ -368,6 +368,7 @@ void SettingsFile::addToRecentlyOpened(File const& path) ValueTree subTree("Path"); subTree.setProperty("Path", path.getFullPathName(), nullptr); subTree.setProperty("Time", Time::getCurrentTime().toMilliseconds(), nullptr); + if(path.isOnRemovableDrive()) subTree.setProperty("Removable", var(1), nullptr); recentlyOpened.addChild(subTree, 0, nullptr); } diff --git a/Source/Utility/stb_image_resize.h b/Source/Utility/stb_image_resize.h new file mode 100644 index 000000000..6f4e8f89c --- /dev/null +++ b/Source/Utility/stb_image_resize.h @@ -0,0 +1,2585 @@ +/* stb_image_resize - v0.90 - public domain image resizing + by Jorge L Rodriguez (@VinoBS) - 2014 + http://github.com/nothings/stb + + Written with emphasis on usability, portability, and efficiency. (No + SIMD or threads, so it be easily outperformed by libs that use those.) + Only scaling and translation is supported, no rotations or shears. + Easy API downsamples w/Mitchell filter, upsamples w/cubic interpolation. + + COMPILING & LINKING + In one C/C++ file that #includes this file, do this: + #define STB_IMAGE_RESIZE_IMPLEMENTATION + before the #include. That will create the implementation in that file. + + QUICKSTART + stbir_resize_uint8( input_pixels , in_w , in_h , 0, + output_pixels, out_w, out_h, 0, num_channels) + stbir_resize_float(...) + stbir_resize_uint8_srgb( input_pixels , in_w , in_h , 0, + output_pixels, out_w, out_h, 0, + num_channels , alpha_chan , 0) + stbir_resize_uint8_srgb_edgemode( + input_pixels , in_w , in_h , 0, + output_pixels, out_w, out_h, 0, + num_channels , alpha_chan , 0, STBIR_EDGE_CLAMP) + // WRAP/REFLECT/ZERO + + FULL API + See the "header file" section of the source for API documentation. + + ADDITIONAL DOCUMENTATION + + SRGB & FLOATING POINT REPRESENTATION + The sRGB functions presume IEEE floating point. If you do not have + IEEE floating point, define STBIR_NON_IEEE_FLOAT. This will use + a slower implementation. + + MEMORY ALLOCATION + The resize functions here perform a single memory allocation using + malloc. To control the memory allocation, before the #include that + triggers the implementation, do: + + #define STBIR_MALLOC(size,context) ... + #define STBIR_FREE(ptr,context) ... + + Each resize function makes exactly one call to malloc/free, so to use + temp memory, store the temp memory in the context and return that. + + ASSERT + Define STBIR_ASSERT(boolval) to override assert() and not use assert.h + + OPTIMIZATION + Define STBIR_SATURATE_INT to compute clamp values in-range using + integer operations instead of float operations. This may be faster + on some platforms. + + DEFAULT FILTERS + For functions which don't provide explicit control over what filters + to use, you can change the compile-time defaults with + + #define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_something + #define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_something + + See stbir_filter in the header-file section for the list of filters. + + NEW FILTERS + A number of 1D filter kernels are used. For a list of + supported filters see the stbir_filter enum. To add a new filter, + write a filter function and add it to stbir__filter_info_table. + + PROGRESS + For interactive use with slow resize operations, you can install + a progress-report callback: + + #define STBIR_PROGRESS_REPORT(val) some_func(val) + + The parameter val is a float which goes from 0 to 1 as progress is made. + + For example: + + static void my_progress_report(float progress); + #define STBIR_PROGRESS_REPORT(val) my_progress_report(val) + + #define STB_IMAGE_RESIZE_IMPLEMENTATION + #include "stb_image_resize.h" + + static void my_progress_report(float progress) + { + printf("Progress: %f%%\n", progress*100); + } + + MAX CHANNELS + If your image has more than 64 channels, define STBIR_MAX_CHANNELS + to the max you'll have. + + ALPHA CHANNEL + Most of the resizing functions provide the ability to control how + the alpha channel of an image is processed. The important things + to know about this: + + 1. The best mathematically-behaved version of alpha to use is + called "premultiplied alpha", in which the other color channels + have had the alpha value multiplied in. If you use premultiplied + alpha, linear filtering (such as image resampling done by this + library, or performed in texture units on GPUs) does the "right + thing". While premultiplied alpha is standard in the movie CGI + industry, it is still uncommon in the videogame/real-time world. + + If you linearly filter non-premultiplied alpha, strange effects + occur. (For example, the average of 1% opaque bright green + and 99% opaque black produces 50% transparent dark green when + non-premultiplied, whereas premultiplied it produces 50% + transparent near-black. The former introduces green energy + that doesn't exist in the source image.) + + 2. Artists should not edit premultiplied-alpha images; artists + want non-premultiplied alpha images. Thus, art tools generally output + non-premultiplied alpha images. + + 3. You will get best results in most cases by converting images + to premultiplied alpha before processing them mathematically. + + 4. If you pass the flag STBIR_FLAG_ALPHA_PREMULTIPLIED, the + resizer does not do anything special for the alpha channel; + it is resampled identically to other channels. This produces + the correct results for premultiplied-alpha images, but produces + less-than-ideal results for non-premultiplied-alpha images. + + 5. If you do not pass the flag STBIR_FLAG_ALPHA_PREMULTIPLIED, + then the resizer weights the contribution of input pixels + based on their alpha values, or, equivalently, it multiplies + the alpha value into the color channels, resamples, then divides + by the resultant alpha value. Input pixels which have alpha=0 do + not contribute at all to output pixels unless _all_ of the input + pixels affecting that output pixel have alpha=0, in which case + the result for that pixel is the same as it would be without + STBIR_FLAG_ALPHA_PREMULTIPLIED. However, this is only true for + input images in integer formats. For input images in float format, + input pixels with alpha=0 have no effect, and output pixels + which have alpha=0 will be 0 in all channels. (For float images, + you can manually achieve the same result by adding a tiny epsilon + value to the alpha channel of every image, and then subtracting + or clamping it at the end.) + + 6. You can suppress the behavior described in #5 and make + all-0-alpha pixels have 0 in all channels by #defining + STBIR_NO_ALPHA_EPSILON. + + 7. You can separately control whether the alpha channel is + interpreted as linear or affected by the colorspace. By default + it is linear; you almost never want to apply the colorspace. + (For example, graphics hardware does not apply sRGB conversion + to the alpha channel.) + + ADDITIONAL CONTRIBUTORS + Sean Barrett: API design, optimizations + + REVISIONS + 0.90 (2014-09-17) first released version + + LICENSE + This software is in the public domain. Where that dedication is not + recognized, you are granted a perpetual, irrevocable license to copy + and modify this file as you see fit. + + TODO + Don't decode all of the image data when only processing a partial tile + Don't use full-width decode buffers when only processing a partial tile + When processing wide images, break processing into tiles so data fits in L1 cache + Installable filters? + Resize that respects alpha test coverage + (Reference code: FloatImage::alphaTestCoverage and FloatImage::scaleAlphaToCoverage: + https://code.google.com/p/nvidia-texture-tools/source/browse/trunk/src/nvimage/FloatImage.cpp ) +*/ + +#ifndef STBIR_INCLUDE_STB_IMAGE_RESIZE_H +#define STBIR_INCLUDE_STB_IMAGE_RESIZE_H + +#ifdef _MSC_VER +typedef unsigned char stbir_uint8; +typedef unsigned short stbir_uint16; +typedef unsigned int stbir_uint32; +#else +#include +typedef uint8_t stbir_uint8; +typedef uint16_t stbir_uint16; +typedef uint32_t stbir_uint32; +#endif + +#ifdef STB_IMAGE_RESIZE_STATIC +#define STBIRDEF static +#else +#ifdef __cplusplus +#define STBIRDEF extern "C" +#else +#define STBIRDEF extern +#endif +#endif + + +////////////////////////////////////////////////////////////////////////////// +// +// Easy-to-use API: +// +// * "input pixels" points to an array of image data with 'num_channels' channels (e.g. RGB=3, RGBA=4) +// * input_w is input image width (x-axis), input_h is input image height (y-axis) +// * stride is the offset between successive rows of image data in memory, in bytes. you can +// specify 0 to mean packed continuously in memory +// * alpha channel is treated identically to other channels. +// * colorspace is linear or sRGB as specified by function name +// * returned result is 1 for success or 0 in case of an error. +// #define STBIR_ASSERT() to trigger an assert on parameter validation errors. +// * Memory required grows approximately linearly with input and output size, but with +// discontinuities at input_w == output_w and input_h == output_h. +// * These functions use a "default" resampling filter defined at compile time. To change the filter, +// you can change the compile-time defaults by #defining STBIR_DEFAULT_FILTER_UPSAMPLE +// and STBIR_DEFAULT_FILTER_DOWNSAMPLE, or you can use the medium-complexity API. + +STBIRDEF int stbir_resize_uint8( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels); + +STBIRDEF int stbir_resize_float( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels); + + +// The following functions interpret image data as gamma-corrected sRGB. +// Specify STBIR_ALPHA_CHANNEL_NONE if you have no alpha channel, +// or otherwise provide the index of the alpha channel. Flags value +// of 0 will probably do the right thing if you're not sure what +// the flags mean. + +#define STBIR_ALPHA_CHANNEL_NONE -1 + +// Set this flag if your texture has premultiplied alpha. Otherwise, stbir will +// use alpha-weighted resampling (effectively premultiplying, resampling, +// then unpremultiplying). +#define STBIR_FLAG_ALPHA_PREMULTIPLIED (1 << 0) +// The specified alpha channel should be handled as gamma-corrected value even +// when doing sRGB operations. +#define STBIR_FLAG_ALPHA_USES_COLORSPACE (1 << 1) + +STBIRDEF int stbir_resize_uint8_srgb(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags); + + +typedef enum +{ + STBIR_EDGE_CLAMP = 1, + STBIR_EDGE_REFLECT = 2, + STBIR_EDGE_WRAP = 3, + STBIR_EDGE_ZERO = 4, +} stbir_edge; + +// This function adds the ability to specify how requests to sample off the edge of the image are handled. +STBIRDEF int stbir_resize_uint8_srgb_edgemode(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode); + +////////////////////////////////////////////////////////////////////////////// +// +// Medium-complexity API +// +// This extends the easy-to-use API as follows: +// +// * Alpha-channel can be processed separately +// * If alpha_channel is not STBIR_ALPHA_CHANNEL_NONE +// * Alpha channel will not be gamma corrected (unless flags&STBIR_FLAG_GAMMA_CORRECT) +// * Filters will be weighted by alpha channel (unless flags&STBIR_FLAG_ALPHA_PREMULTIPLIED) +// * Filter can be selected explicitly +// * uint16 image type +// * sRGB colorspace available for all types +// * context parameter for passing to STBIR_MALLOC + +typedef enum +{ + STBIR_FILTER_DEFAULT = 0, // use same filter type that easy-to-use API chooses + STBIR_FILTER_BOX = 1, // A trapezoid w/1-pixel wide ramps, same result as box for integer scale ratios + STBIR_FILTER_TRIANGLE = 2, // On upsampling, produces same results as bilinear texture filtering + STBIR_FILTER_CUBICBSPLINE = 3, // The cubic b-spline (aka Mitchell-Netrevalli with B=1,C=0), gaussian-esque + STBIR_FILTER_CATMULLROM = 4, // An interpolating cubic spline + STBIR_FILTER_MITCHELL = 5, // Mitchell-Netrevalli filter with B=1/3, C=1/3 +} stbir_filter; + +typedef enum +{ + STBIR_COLORSPACE_LINEAR, + STBIR_COLORSPACE_SRGB, + + STBIR_MAX_COLORSPACES, +} stbir_colorspace; + +// The following functions are all identical except for the type of the image data + +STBIRDEF int stbir_resize_uint8_generic( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context); + +STBIRDEF int stbir_resize_uint16_generic(const stbir_uint16 *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + stbir_uint16 *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context); + +STBIRDEF int stbir_resize_float_generic( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context); + + + +////////////////////////////////////////////////////////////////////////////// +// +// Full-complexity API +// +// This extends the medium API as follows: +// +// * uint32 image type +// * not typesafe +// * separate filter types for each axis +// * separate edge modes for each axis +// * can specify scale explicitly for subpixel correctness +// * can specify image source tile using texture coordinates + +typedef enum +{ + STBIR_TYPE_UINT8 , + STBIR_TYPE_UINT16, + STBIR_TYPE_UINT32, + STBIR_TYPE_FLOAT , + + STBIR_MAX_TYPES +} stbir_datatype; + +STBIRDEF int stbir_resize( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context); + +STBIRDEF int stbir_resize_subpixel(const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float x_scale, float y_scale, + float x_offset, float y_offset); + +STBIRDEF int stbir_resize_region( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float s0, float t0, float s1, float t1); +// (s0, t0) & (s1, t1) are the top-left and bottom right corner (uv addressing style: [0, 1]x[0, 1]) of a region of the input image to use. + +// +// +//// end header file ///////////////////////////////////////////////////// +#endif // STBIR_INCLUDE_STB_IMAGE_RESIZE_H + + + + + +#ifdef STB_IMAGE_RESIZE_IMPLEMENTATION + +#ifndef STBIR_ASSERT +#include +#define STBIR_ASSERT(x) assert(x) +#endif + +#ifdef STBIR_DEBUG +#define STBIR__DEBUG_ASSERT STBIR_ASSERT +#else +#define STBIR__DEBUG_ASSERT +#endif + +// If you hit this it means I haven't done it yet. +#define STBIR__UNIMPLEMENTED(x) STBIR_ASSERT(!(x)) + +// For memset +#include + +#include + +#ifndef STBIR_MALLOC +#include +#define STBIR_MALLOC(size,c) malloc(size) +#define STBIR_FREE(ptr,c) free(ptr) +#endif + +#ifndef _MSC_VER +#ifdef __cplusplus +#define stbir__inline inline +#else +#define stbir__inline +#endif +#else +#define stbir__inline __forceinline +#endif + + +// should produce compiler error if size is wrong +typedef unsigned char stbir__validate_uint32[sizeof(stbir_uint32) == 4 ? 1 : -1]; + +#ifdef _MSC_VER +#define STBIR__NOTUSED(v) (void)(v) +#else +#define STBIR__NOTUSED(v) (void)sizeof(v) +#endif + +#define STBIR__ARRAY_SIZE(a) (sizeof((a))/sizeof((a)[0])) + +#ifndef STBIR_DEFAULT_FILTER_UPSAMPLE +#define STBIR_DEFAULT_FILTER_UPSAMPLE STBIR_FILTER_CATMULLROM +#endif + +#ifndef STBIR_DEFAULT_FILTER_DOWNSAMPLE +#define STBIR_DEFAULT_FILTER_DOWNSAMPLE STBIR_FILTER_MITCHELL +#endif + +#ifndef STBIR_PROGRESS_REPORT +#define STBIR_PROGRESS_REPORT(float_0_to_1) +#endif + +#ifndef STBIR_MAX_CHANNELS +#define STBIR_MAX_CHANNELS 64 +#endif + +#if STBIR_MAX_CHANNELS > 65536 +#error "Too many channels; STBIR_MAX_CHANNELS must be no more than 65536." +// because we store the indices in 16-bit variables +#endif + +// This value is added to alpha just before premultiplication to avoid +// zeroing out color values. It is equivalent to 2^-80. If you don't want +// that behavior (it may interfere if you have floating point images with +// very small alpha values) then you can define STBIR_NO_ALPHA_EPSILON to +// disable it. +#ifndef STBIR_ALPHA_EPSILON +#define STBIR_ALPHA_EPSILON ((float)1 / (1 << 20) / (1 << 20) / (1 << 20) / (1 << 20)) +#endif + + + +#ifdef _MSC_VER +#define STBIR__UNUSED_PARAM(v) (void)(v) +#else +#define STBIR__UNUSED_PARAM(v) (void)sizeof(v) +#endif + +// must match stbir_datatype +static unsigned char stbir__type_size[] = { + 1, // STBIR_TYPE_UINT8 + 2, // STBIR_TYPE_UINT16 + 4, // STBIR_TYPE_UINT32 + 4, // STBIR_TYPE_FLOAT +}; + +// Kernel function centered at 0 +typedef float (stbir__kernel_fn)(float x, float scale); +typedef float (stbir__support_fn)(float scale); + +typedef struct +{ + stbir__kernel_fn* kernel; + stbir__support_fn* support; +} stbir__filter_info; + +// When upsampling, the contributors are which source pixels contribute. +// When downsampling, the contributors are which destination pixels are contributed to. +typedef struct +{ + int n0; // First contributing pixel + int n1; // Last contributing pixel +} stbir__contributors; + +typedef struct +{ + const void* input_data; + int input_w; + int input_h; + int input_stride_bytes; + + void* output_data; + int output_w; + int output_h; + int output_stride_bytes; + + float s0, t0, s1, t1; + + float horizontal_shift; // Units: output pixels + float vertical_shift; // Units: output pixels + float horizontal_scale; + float vertical_scale; + + int channels; + int alpha_channel; + stbir_uint32 flags; + stbir_datatype type; + stbir_filter horizontal_filter; + stbir_filter vertical_filter; + stbir_edge edge_horizontal; + stbir_edge edge_vertical; + stbir_colorspace colorspace; + + stbir__contributors* horizontal_contributors; + float* horizontal_coefficients; + + stbir__contributors* vertical_contributors; + float* vertical_coefficients; + + int decode_buffer_pixels; + float* decode_buffer; + + float* horizontal_buffer; + + // cache these because ceil/floor are inexplicably showing up in profile + int horizontal_coefficient_width; + int vertical_coefficient_width; + int horizontal_filter_pixel_width; + int vertical_filter_pixel_width; + int horizontal_filter_pixel_margin; + int vertical_filter_pixel_margin; + int horizontal_num_contributors; + int vertical_num_contributors; + + int ring_buffer_length_bytes; // The length of an individual entry in the ring buffer. The total number of ring buffers is stbir__get_filter_pixel_width(filter) + int ring_buffer_first_scanline; + int ring_buffer_last_scanline; + int ring_buffer_begin_index; + float* ring_buffer; + + float* encode_buffer; // A temporary buffer to store floats so we don't lose precision while we do multiply-adds. + + int horizontal_contributors_size; + int horizontal_coefficients_size; + int vertical_contributors_size; + int vertical_coefficients_size; + int decode_buffer_size; + int horizontal_buffer_size; + int ring_buffer_size; + int encode_buffer_size; +} stbir__info; + +static stbir__inline int stbir__min(int a, int b) +{ + return a < b ? a : b; +} + +static stbir__inline int stbir__max(int a, int b) +{ + return a > b ? a : b; +} + +static stbir__inline float stbir__saturate(float x) +{ + if (x < 0) + return 0; + + if (x > 1) + return 1; + + return x; +} + +#ifdef STBIR_SATURATE_INT +static stbir__inline stbir_uint8 stbir__saturate8(int x) +{ + if ((unsigned int) x <= 255) + return x; + + if (x < 0) + return 0; + + return 255; +} + +static stbir__inline stbir_uint16 stbir__saturate16(int x) +{ + if ((unsigned int) x <= 65535) + return x; + + if (x < 0) + return 0; + + return 65535; +} +#endif + +static float stbir__srgb_uchar_to_linear_float[256] = { + 0.000000f, 0.000304f, 0.000607f, 0.000911f, 0.001214f, 0.001518f, 0.001821f, 0.002125f, 0.002428f, 0.002732f, 0.003035f, + 0.003347f, 0.003677f, 0.004025f, 0.004391f, 0.004777f, 0.005182f, 0.005605f, 0.006049f, 0.006512f, 0.006995f, 0.007499f, + 0.008023f, 0.008568f, 0.009134f, 0.009721f, 0.010330f, 0.010960f, 0.011612f, 0.012286f, 0.012983f, 0.013702f, 0.014444f, + 0.015209f, 0.015996f, 0.016807f, 0.017642f, 0.018500f, 0.019382f, 0.020289f, 0.021219f, 0.022174f, 0.023153f, 0.024158f, + 0.025187f, 0.026241f, 0.027321f, 0.028426f, 0.029557f, 0.030713f, 0.031896f, 0.033105f, 0.034340f, 0.035601f, 0.036889f, + 0.038204f, 0.039546f, 0.040915f, 0.042311f, 0.043735f, 0.045186f, 0.046665f, 0.048172f, 0.049707f, 0.051269f, 0.052861f, + 0.054480f, 0.056128f, 0.057805f, 0.059511f, 0.061246f, 0.063010f, 0.064803f, 0.066626f, 0.068478f, 0.070360f, 0.072272f, + 0.074214f, 0.076185f, 0.078187f, 0.080220f, 0.082283f, 0.084376f, 0.086500f, 0.088656f, 0.090842f, 0.093059f, 0.095307f, + 0.097587f, 0.099899f, 0.102242f, 0.104616f, 0.107023f, 0.109462f, 0.111932f, 0.114435f, 0.116971f, 0.119538f, 0.122139f, + 0.124772f, 0.127438f, 0.130136f, 0.132868f, 0.135633f, 0.138432f, 0.141263f, 0.144128f, 0.147027f, 0.149960f, 0.152926f, + 0.155926f, 0.158961f, 0.162029f, 0.165132f, 0.168269f, 0.171441f, 0.174647f, 0.177888f, 0.181164f, 0.184475f, 0.187821f, + 0.191202f, 0.194618f, 0.198069f, 0.201556f, 0.205079f, 0.208637f, 0.212231f, 0.215861f, 0.219526f, 0.223228f, 0.226966f, + 0.230740f, 0.234551f, 0.238398f, 0.242281f, 0.246201f, 0.250158f, 0.254152f, 0.258183f, 0.262251f, 0.266356f, 0.270498f, + 0.274677f, 0.278894f, 0.283149f, 0.287441f, 0.291771f, 0.296138f, 0.300544f, 0.304987f, 0.309469f, 0.313989f, 0.318547f, + 0.323143f, 0.327778f, 0.332452f, 0.337164f, 0.341914f, 0.346704f, 0.351533f, 0.356400f, 0.361307f, 0.366253f, 0.371238f, + 0.376262f, 0.381326f, 0.386430f, 0.391573f, 0.396755f, 0.401978f, 0.407240f, 0.412543f, 0.417885f, 0.423268f, 0.428691f, + 0.434154f, 0.439657f, 0.445201f, 0.450786f, 0.456411f, 0.462077f, 0.467784f, 0.473532f, 0.479320f, 0.485150f, 0.491021f, + 0.496933f, 0.502887f, 0.508881f, 0.514918f, 0.520996f, 0.527115f, 0.533276f, 0.539480f, 0.545725f, 0.552011f, 0.558340f, + 0.564712f, 0.571125f, 0.577581f, 0.584078f, 0.590619f, 0.597202f, 0.603827f, 0.610496f, 0.617207f, 0.623960f, 0.630757f, + 0.637597f, 0.644480f, 0.651406f, 0.658375f, 0.665387f, 0.672443f, 0.679543f, 0.686685f, 0.693872f, 0.701102f, 0.708376f, + 0.715694f, 0.723055f, 0.730461f, 0.737911f, 0.745404f, 0.752942f, 0.760525f, 0.768151f, 0.775822f, 0.783538f, 0.791298f, + 0.799103f, 0.806952f, 0.814847f, 0.822786f, 0.830770f, 0.838799f, 0.846873f, 0.854993f, 0.863157f, 0.871367f, 0.879622f, + 0.887923f, 0.896269f, 0.904661f, 0.913099f, 0.921582f, 0.930111f, 0.938686f, 0.947307f, 0.955974f, 0.964686f, 0.973445f, + 0.982251f, 0.991102f, 1.0f +}; + +static float stbir__srgb_to_linear(float f) +{ + if (f <= 0.04045f) + return f / 12.92f; + else + return (float)pow((f + 0.055f) / 1.055f, 2.4f); +} + +static float stbir__linear_to_srgb(float f) +{ + if (f <= 0.0031308f) + return f * 12.92f; + else + return 1.055f * (float)pow(f, 1 / 2.4f) - 0.055f; +} + +#ifndef STBIR_NON_IEEE_FLOAT +// From https://gist.github.com/rygorous/2203834 + +typedef union +{ + stbir_uint32 u; + float f; +} stbir__FP32; + +static const stbir_uint32 fp32_to_srgb8_tab4[104] = { + 0x0073000d, 0x007a000d, 0x0080000d, 0x0087000d, 0x008d000d, 0x0094000d, 0x009a000d, 0x00a1000d, + 0x00a7001a, 0x00b4001a, 0x00c1001a, 0x00ce001a, 0x00da001a, 0x00e7001a, 0x00f4001a, 0x0101001a, + 0x010e0033, 0x01280033, 0x01410033, 0x015b0033, 0x01750033, 0x018f0033, 0x01a80033, 0x01c20033, + 0x01dc0067, 0x020f0067, 0x02430067, 0x02760067, 0x02aa0067, 0x02dd0067, 0x03110067, 0x03440067, + 0x037800ce, 0x03df00ce, 0x044600ce, 0x04ad00ce, 0x051400ce, 0x057b00c5, 0x05dd00bc, 0x063b00b5, + 0x06970158, 0x07420142, 0x07e30130, 0x087b0120, 0x090b0112, 0x09940106, 0x0a1700fc, 0x0a9500f2, + 0x0b0f01cb, 0x0bf401ae, 0x0ccb0195, 0x0d950180, 0x0e56016e, 0x0f0d015e, 0x0fbc0150, 0x10630143, + 0x11070264, 0x1238023e, 0x1357021d, 0x14660201, 0x156601e9, 0x165a01d3, 0x174401c0, 0x182401af, + 0x18fe0331, 0x1a9602fe, 0x1c1502d2, 0x1d7e02ad, 0x1ed4028d, 0x201a0270, 0x21520256, 0x227d0240, + 0x239f0443, 0x25c003fe, 0x27bf03c4, 0x29a10392, 0x2b6a0367, 0x2d1d0341, 0x2ebe031f, 0x304d0300, + 0x31d105b0, 0x34a80555, 0x37520507, 0x39d504c5, 0x3c37048b, 0x3e7c0458, 0x40a8042a, 0x42bd0401, + 0x44c20798, 0x488e071e, 0x4c1c06b6, 0x4f76065d, 0x52a50610, 0x55ac05cc, 0x5892058f, 0x5b590559, + 0x5e0c0a23, 0x631c0980, 0x67db08f6, 0x6c55087f, 0x70940818, 0x74a007bd, 0x787d076c, 0x7c330723, +}; + +static stbir_uint8 stbir__linear_to_srgb_uchar(float in) +{ + static const stbir__FP32 almostone = { 0x3f7fffff }; // 1-eps + static const stbir__FP32 minval = { (127-13) << 23 }; + stbir_uint32 tab,bias,scale,t; + stbir__FP32 f; + + // Clamp to [2^(-13), 1-eps]; these two values map to 0 and 1, respectively. + // The tests are carefully written so that NaNs map to 0, same as in the reference + // implementation. + if (!(in > minval.f)) // written this way to catch NaNs + in = minval.f; + if (in > almostone.f) + in = almostone.f; + + // Do the table lookup and unpack bias, scale + f.f = in; + tab = fp32_to_srgb8_tab4[(f.u - minval.u) >> 20]; + bias = (tab >> 16) << 9; + scale = tab & 0xffff; + + // Grab next-highest mantissa bits and perform linear interpolation + t = (f.u >> 12) & 0xff; + return (unsigned char) ((bias + scale*t) >> 16); +} + +#else +// sRGB transition values, scaled by 1<<28 +static int stbir__srgb_offset_to_linear_scaled[256] = +{ + 0, 40738, 122216, 203693, 285170, 366648, 448125, 529603, + 611080, 692557, 774035, 855852, 942009, 1033024, 1128971, 1229926, + 1335959, 1447142, 1563542, 1685229, 1812268, 1944725, 2082664, 2226148, + 2375238, 2529996, 2690481, 2856753, 3028870, 3206888, 3390865, 3580856, + 3776916, 3979100, 4187460, 4402049, 4622919, 4850123, 5083710, 5323731, + 5570236, 5823273, 6082892, 6349140, 6622065, 6901714, 7188133, 7481369, + 7781466, 8088471, 8402427, 8723380, 9051372, 9386448, 9728650, 10078021, + 10434603, 10798439, 11169569, 11548036, 11933879, 12327139, 12727857, 13136073, + 13551826, 13975156, 14406100, 14844697, 15290987, 15745007, 16206795, 16676389, + 17153826, 17639142, 18132374, 18633560, 19142734, 19659934, 20185196, 20718552, + 21260042, 21809696, 22367554, 22933648, 23508010, 24090680, 24681686, 25281066, + 25888850, 26505076, 27129772, 27762974, 28404716, 29055026, 29713942, 30381490, + 31057708, 31742624, 32436272, 33138682, 33849884, 34569912, 35298800, 36036568, + 36783260, 37538896, 38303512, 39077136, 39859796, 40651528, 41452360, 42262316, + 43081432, 43909732, 44747252, 45594016, 46450052, 47315392, 48190064, 49074096, + 49967516, 50870356, 51782636, 52704392, 53635648, 54576432, 55526772, 56486700, + 57456236, 58435408, 59424248, 60422780, 61431036, 62449032, 63476804, 64514376, + 65561776, 66619028, 67686160, 68763192, 69850160, 70947088, 72053992, 73170912, + 74297864, 75434880, 76581976, 77739184, 78906536, 80084040, 81271736, 82469648, + 83677792, 84896192, 86124888, 87363888, 88613232, 89872928, 91143016, 92423512, + 93714432, 95015816, 96327688, 97650056, 98982952, 100326408, 101680440, 103045072, + 104420320, 105806224, 107202800, 108610064, 110028048, 111456776, 112896264, 114346544, + 115807632, 117279552, 118762328, 120255976, 121760536, 123276016, 124802440, 126339832, + 127888216, 129447616, 131018048, 132599544, 134192112, 135795792, 137410592, 139036528, + 140673648, 142321952, 143981456, 145652208, 147334208, 149027488, 150732064, 152447968, + 154175200, 155913792, 157663776, 159425168, 161197984, 162982240, 164777968, 166585184, + 168403904, 170234160, 172075968, 173929344, 175794320, 177670896, 179559120, 181458992, + 183370528, 185293776, 187228736, 189175424, 191133888, 193104112, 195086128, 197079968, + 199085648, 201103184, 203132592, 205173888, 207227120, 209292272, 211369392, 213458480, + 215559568, 217672656, 219797792, 221934976, 224084240, 226245600, 228419056, 230604656, + 232802400, 235012320, 237234432, 239468736, 241715280, 243974080, 246245120, 248528464, + 250824112, 253132064, 255452368, 257785040, 260130080, 262487520, 264857376, 267239664, +}; + +static stbir_uint8 stbir__linear_to_srgb_uchar(float f) +{ + int x = (int) (f * (1 << 28)); // has headroom so you don't need to clamp + int v = 0; + int i; + + // Refine the guess with a short binary search. + i = v + 128; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 64; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 32; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 16; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 8; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 4; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 2; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + i = v + 1; if (x >= stbir__srgb_offset_to_linear_scaled[i]) v = i; + + return (stbir_uint8) v; +} +#endif + +static float stbir__filter_trapezoid(float x, float scale) +{ + float halfscale = scale / 2; + float t = 0.5f + halfscale; + STBIR__DEBUG_ASSERT(scale <= 1); + + x = (float)fabs(x); + + if (x >= t) + return 0; + else + { + float r = 0.5f - halfscale; + if (x <= r) + return 1; + else + return (t - x) / scale; + } +} + +static float stbir__support_trapezoid(float scale) +{ + STBIR__DEBUG_ASSERT(scale <= 1); + return 0.5f + scale / 2; +} + +static float stbir__filter_triangle(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x <= 1.0f) + return 1 - x; + else + return 0; +} + +static float stbir__filter_cubic(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x < 1.0f) + return (4 + x*x*(3*x - 6))/6; + else if (x < 2.0f) + return (8 + x*(-12 + x*(6 - x)))/6; + + return (0.0f); +} + +static float stbir__filter_catmullrom(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x < 1.0f) + return 1 - x*x*(2.5f - 1.5f*x); + else if (x < 2.0f) + return 2 - x*(4 + x*(0.5f*x - 2.5f)); + + return (0.0f); +} + +static float stbir__filter_mitchell(float x, float s) +{ + STBIR__UNUSED_PARAM(s); + + x = (float)fabs(x); + + if (x < 1.0f) + return (16 + x*x*(21 * x - 36))/18; + else if (x < 2.0f) + return (32 + x*(-60 + x*(36 - 7*x)))/18; + + return (0.0f); +} + +static float stbir__support_zero(float s) +{ + STBIR__UNUSED_PARAM(s); + return 0; +} + +static float stbir__support_one(float s) +{ + STBIR__UNUSED_PARAM(s); + return 1; +} + +static float stbir__support_two(float s) +{ + STBIR__UNUSED_PARAM(s); + return 2; +} + +static stbir__filter_info stbir__filter_info_table[] = { + { NULL, stbir__support_zero }, + { stbir__filter_trapezoid, stbir__support_trapezoid }, + { stbir__filter_triangle, stbir__support_one }, + { stbir__filter_cubic, stbir__support_two }, + { stbir__filter_catmullrom, stbir__support_two }, + { stbir__filter_mitchell, stbir__support_two }, +}; + +stbir__inline static int stbir__use_upsampling(float ratio) +{ + return ratio > 1; +} + +stbir__inline static int stbir__use_width_upsampling(stbir__info* stbir_info) +{ + return stbir__use_upsampling(stbir_info->horizontal_scale); +} + +stbir__inline static int stbir__use_height_upsampling(stbir__info* stbir_info) +{ + return stbir__use_upsampling(stbir_info->vertical_scale); +} + +// This is the maximum number of input samples that can affect an output sample +// with the given filter +static int stbir__get_filter_pixel_width(stbir_filter filter, float scale) +{ + STBIR_ASSERT(filter != 0); + STBIR_ASSERT(filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); + + if (stbir__use_upsampling(scale)) + return (int)ceil(stbir__filter_info_table[filter].support(1/scale) * 2); + else + return (int)ceil(stbir__filter_info_table[filter].support(scale) * 2 / scale); +} + +// This is how much to expand buffers to account for filters seeking outside +// the image boundaries. +static int stbir__get_filter_pixel_margin(stbir_filter filter, float scale) +{ + return stbir__get_filter_pixel_width(filter, scale) / 2; +} + +static int stbir__get_coefficient_width(stbir_filter filter, float scale) +{ + if (stbir__use_upsampling(scale)) + return (int)ceil(stbir__filter_info_table[filter].support(1 / scale) * 2); + else + return (int)ceil(stbir__filter_info_table[filter].support(scale) * 2); +} + +static int stbir__get_contributors(float scale, stbir_filter filter, int input_size, int output_size) +{ + if (stbir__use_upsampling(scale)) + return output_size; + else + return (input_size + stbir__get_filter_pixel_margin(filter, scale) * 2); +} + +static int stbir__get_total_horizontal_coefficients(stbir__info* info) +{ + return info->horizontal_num_contributors + * stbir__get_coefficient_width (info->horizontal_filter, info->horizontal_scale); +} + +static int stbir__get_total_vertical_coefficients(stbir__info* info) +{ + return info->vertical_num_contributors + * stbir__get_coefficient_width (info->vertical_filter, info->vertical_scale); +} + +static stbir__contributors* stbir__get_contributor(stbir__contributors* contributors, int n) +{ + return &contributors[n]; +} + +// For perf reasons this code is duplicated in stbir__resample_horizontal_upsample/downsample, +// if you change it here change it there too. +static float* stbir__get_coefficient(float* coefficients, stbir_filter filter, float scale, int n, int c) +{ + int width = stbir__get_coefficient_width(filter, scale); + return &coefficients[width*n + c]; +} + +static int stbir__edge_wrap_slow(stbir_edge edge, int n, int max) +{ + switch (edge) + { + case STBIR_EDGE_ZERO: + return 0; // we'll decode the wrong pixel here, and then overwrite with 0s later + + case STBIR_EDGE_CLAMP: + if (n < 0) + return 0; + + if (n >= max) + return max - 1; + + return n; // NOTREACHED + + case STBIR_EDGE_REFLECT: + { + if (n < 0) + { + if (n < max) + return -n; + else + return max - 1; + } + + if (n >= max) + { + int max2 = max * 2; + if (n >= max2) + return 0; + else + return max2 - n - 1; + } + + return n; // NOTREACHED + } + + case STBIR_EDGE_WRAP: + if (n >= 0) + return (n % max); + else + { + int m = (-n) % max; + + if (m != 0) + m = max - m; + + return (m); + } + return n; // NOTREACHED + + default: + STBIR__UNIMPLEMENTED("Unimplemented edge type"); + return 0; + } +} + +stbir__inline static int stbir__edge_wrap(stbir_edge edge, int n, int max) +{ + // avoid per-pixel switch + if (n >= 0 && n < max) + return n; + return stbir__edge_wrap_slow(edge, n, max); +} + +// What input pixels contribute to this output pixel? +static void stbir__calculate_sample_range_upsample(int n, float out_filter_radius, float scale_ratio, float out_shift, int* in_first_pixel, int* in_last_pixel, float* in_center_of_out) +{ + float out_pixel_center = (float)n + 0.5f; + float out_pixel_influence_lowerbound = out_pixel_center - out_filter_radius; + float out_pixel_influence_upperbound = out_pixel_center + out_filter_radius; + + float in_pixel_influence_lowerbound = (out_pixel_influence_lowerbound + out_shift) / scale_ratio; + float in_pixel_influence_upperbound = (out_pixel_influence_upperbound + out_shift) / scale_ratio; + + *in_center_of_out = (out_pixel_center + out_shift) / scale_ratio; + *in_first_pixel = (int)(floor(in_pixel_influence_lowerbound + 0.5)); + *in_last_pixel = (int)(floor(in_pixel_influence_upperbound - 0.5)); +} + +// What output pixels does this input pixel contribute to? +static void stbir__calculate_sample_range_downsample(int n, float in_pixels_radius, float scale_ratio, float out_shift, int* out_first_pixel, int* out_last_pixel, float* out_center_of_in) +{ + float in_pixel_center = (float)n + 0.5f; + float in_pixel_influence_lowerbound = in_pixel_center - in_pixels_radius; + float in_pixel_influence_upperbound = in_pixel_center + in_pixels_radius; + + float out_pixel_influence_lowerbound = in_pixel_influence_lowerbound * scale_ratio - out_shift; + float out_pixel_influence_upperbound = in_pixel_influence_upperbound * scale_ratio - out_shift; + + *out_center_of_in = in_pixel_center * scale_ratio - out_shift; + *out_first_pixel = (int)(floor(out_pixel_influence_lowerbound + 0.5)); + *out_last_pixel = (int)(floor(out_pixel_influence_upperbound - 0.5)); +} + +static void stbir__calculate_coefficients_upsample(stbir__info* stbir_info, stbir_filter filter, float scale, int in_first_pixel, int in_last_pixel, float in_center_of_out, stbir__contributors* contributor, float* coefficient_group) +{ + int i; + float total_filter = 0; + float filter_scale; + + STBIR__DEBUG_ASSERT(in_last_pixel - in_first_pixel <= (int)ceil(stbir__filter_info_table[filter].support(1/scale) * 2)); // Taken directly from stbir__get_coefficient_width() which we can't call because we don't know if we're horizontal or vertical. + + contributor->n0 = in_first_pixel; + contributor->n1 = in_last_pixel; + + STBIR__DEBUG_ASSERT(contributor->n1 >= contributor->n0); + + for (i = 0; i <= in_last_pixel - in_first_pixel; i++) + { + float in_pixel_center = (float)(i + in_first_pixel) + 0.5f; + coefficient_group[i] = stbir__filter_info_table[filter].kernel(in_center_of_out - in_pixel_center, 1 / scale); + + // If the coefficient is zero, skip it. (Don't do the <0 check here, we want the influence of those outside pixels.) + if (i == 0 && !coefficient_group[i]) + { + contributor->n0 = ++in_first_pixel; + i--; + continue; + } + + total_filter += coefficient_group[i]; + } + + STBIR__DEBUG_ASSERT(stbir__filter_info_table[filter].kernel((float)(in_last_pixel + 1) + 0.5f - in_center_of_out, 1/scale) == 0); + + STBIR__DEBUG_ASSERT(total_filter > 0.9); + STBIR__DEBUG_ASSERT(total_filter < 1.1f); // Make sure it's not way off. + + // Make sure the sum of all coefficients is 1. + filter_scale = 1 / total_filter; + + for (i = 0; i <= in_last_pixel - in_first_pixel; i++) + coefficient_group[i] *= filter_scale; + + for (i = in_last_pixel - in_first_pixel; i >= 0; i--) + { + if (coefficient_group[i]) + break; + + // This line has no weight. We can skip it. + contributor->n1 = contributor->n0 + i - 1; + } +} + +static void stbir__calculate_coefficients_downsample(stbir__info* stbir_info, stbir_filter filter, float scale_ratio, int out_first_pixel, int out_last_pixel, float out_center_of_in, stbir__contributors* contributor, float* coefficient_group) +{ + int i; + + STBIR__DEBUG_ASSERT(out_last_pixel - out_first_pixel <= (int)ceil(stbir__filter_info_table[filter].support(scale_ratio) * 2)); // Taken directly from stbir__get_coefficient_width() which we can't call because we don't know if we're horizontal or vertical. + + contributor->n0 = out_first_pixel; + contributor->n1 = out_last_pixel; + + STBIR__DEBUG_ASSERT(contributor->n1 >= contributor->n0); + + for (i = 0; i <= out_last_pixel - out_first_pixel; i++) + { + float out_pixel_center = (float)(i + out_first_pixel) + 0.5f; + float x = out_pixel_center - out_center_of_in; + coefficient_group[i] = stbir__filter_info_table[filter].kernel(x, scale_ratio) * scale_ratio; + } + + STBIR__DEBUG_ASSERT(stbir__filter_info_table[filter].kernel((float)(out_last_pixel + 1) + 0.5f - out_center_of_in, scale_ratio) == 0); + + for (i = out_last_pixel - out_first_pixel; i >= 0; i--) + { + if (coefficient_group[i]) + break; + + // This line has no weight. We can skip it. + contributor->n1 = contributor->n0 + i - 1; + } +} + +static void stbir__normalize_downsample_coefficients(stbir__info* stbir_info, stbir__contributors* contributors, float* coefficients, stbir_filter filter, float scale_ratio, float shift, int input_size, int output_size) +{ + int num_contributors = stbir__get_contributors(scale_ratio, filter, input_size, output_size); + int num_coefficients = stbir__get_coefficient_width(filter, scale_ratio); + int i, j; + int skip; + + for (i = 0; i < output_size; i++) + { + float scale; + float total = 0; + + for (j = 0; j < num_contributors; j++) + { + if (i >= contributors[j].n0 && i <= contributors[j].n1) + { + float coefficient = *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i - contributors[j].n0); + total += coefficient; + } + else if (i < contributors[j].n0) + break; + } + + STBIR__DEBUG_ASSERT(total > 0.9f); + STBIR__DEBUG_ASSERT(total < 1.1f); + + scale = 1 / total; + + for (j = 0; j < num_contributors; j++) + { + if (i >= contributors[j].n0 && i <= contributors[j].n1) + *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i - contributors[j].n0) *= scale; + else if (i < contributors[j].n0) + break; + } + } + + // Optimize: Skip zero coefficients and contributions outside of image bounds. + // Do this after normalizing because normalization depends on the n0/n1 values. + for (j = 0; j < num_contributors; j++) + { + int range, max, width; + + skip = 0; + while (*stbir__get_coefficient(coefficients, filter, scale_ratio, j, skip) == 0) + skip++; + + contributors[j].n0 += skip; + + while (contributors[j].n0 < 0) + { + contributors[j].n0++; + skip++; + } + + range = contributors[j].n1 - contributors[j].n0 + 1; + max = stbir__min(num_coefficients, range); + + width = stbir__get_coefficient_width(filter, scale_ratio); + for (i = 0; i < max; i++) + { + if (i + skip >= width) + break; + + *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i) = *stbir__get_coefficient(coefficients, filter, scale_ratio, j, i + skip); + } + + continue; + } + + // Using min to avoid writing into invalid pixels. + for (i = 0; i < num_contributors; i++) + contributors[i].n1 = stbir__min(contributors[i].n1, output_size - 1); +} + +// Each scan line uses the same kernel values so we should calculate the kernel +// values once and then we can use them for every scan line. +static void stbir__calculate_filters(stbir__info* stbir_info, stbir__contributors* contributors, float* coefficients, stbir_filter filter, float scale_ratio, float shift, int input_size, int output_size) +{ + int n; + int total_contributors = stbir__get_contributors(scale_ratio, filter, input_size, output_size); + + if (stbir__use_upsampling(scale_ratio)) + { + float out_pixels_radius = stbir__filter_info_table[filter].support(1 / scale_ratio) * scale_ratio; + + // Looping through out pixels + for (n = 0; n < total_contributors; n++) + { + float in_center_of_out; // Center of the current out pixel in the in pixel space + int in_first_pixel, in_last_pixel; + + stbir__calculate_sample_range_upsample(n, out_pixels_radius, scale_ratio, shift, &in_first_pixel, &in_last_pixel, &in_center_of_out); + + stbir__calculate_coefficients_upsample(stbir_info, filter, scale_ratio, in_first_pixel, in_last_pixel, in_center_of_out, stbir__get_contributor(contributors, n), stbir__get_coefficient(coefficients, filter, scale_ratio, n, 0)); + } + } + else + { + float in_pixels_radius = stbir__filter_info_table[filter].support(scale_ratio) / scale_ratio; + + // Looping through in pixels + for (n = 0; n < total_contributors; n++) + { + float out_center_of_in; // Center of the current out pixel in the in pixel space + int out_first_pixel, out_last_pixel; + int n_adjusted = n - stbir__get_filter_pixel_margin(filter, scale_ratio); + + stbir__calculate_sample_range_downsample(n_adjusted, in_pixels_radius, scale_ratio, shift, &out_first_pixel, &out_last_pixel, &out_center_of_in); + + stbir__calculate_coefficients_downsample(stbir_info, filter, scale_ratio, out_first_pixel, out_last_pixel, out_center_of_in, stbir__get_contributor(contributors, n), stbir__get_coefficient(coefficients, filter, scale_ratio, n, 0)); + } + + stbir__normalize_downsample_coefficients(stbir_info, contributors, coefficients, filter, scale_ratio, shift, input_size, output_size); + } +} + +static float* stbir__get_decode_buffer(stbir__info* stbir_info) +{ + // The 0 index of the decode buffer starts after the margin. This makes + // it okay to use negative indexes on the decode buffer. + return &stbir_info->decode_buffer[stbir_info->horizontal_filter_pixel_margin * stbir_info->channels]; +} + +#define STBIR__DECODE(type, colorspace) ((type) * (STBIR_MAX_COLORSPACES) + (colorspace)) + +static void stbir__decode_scanline(stbir__info* stbir_info, int n) +{ + int c; + int channels = stbir_info->channels; + int alpha_channel = stbir_info->alpha_channel; + int type = stbir_info->type; + int colorspace = stbir_info->colorspace; + int input_w = stbir_info->input_w; + int input_stride_bytes = stbir_info->input_stride_bytes; + float* decode_buffer = stbir__get_decode_buffer(stbir_info); + stbir_edge edge_horizontal = stbir_info->edge_horizontal; + stbir_edge edge_vertical = stbir_info->edge_vertical; + int in_buffer_row_offset = stbir__edge_wrap(edge_vertical, n, stbir_info->input_h) * input_stride_bytes; + const void* input_data = (char *) stbir_info->input_data + in_buffer_row_offset; + int max_x = input_w + stbir_info->horizontal_filter_pixel_margin; + int decode = STBIR__DECODE(type, colorspace); + + int x = -stbir_info->horizontal_filter_pixel_margin; + + // special handling for STBIR_EDGE_ZERO because it needs to return an item that doesn't appear in the input, + // and we want to avoid paying overhead on every pixel if not STBIR_EDGE_ZERO + if (edge_vertical == STBIR_EDGE_ZERO && (n < 0 || n >= stbir_info->input_h)) + { + for (; x < max_x; x++) + for (c = 0; c < channels; c++) + decode_buffer[x*channels + c] = 0; + return; + } + + switch (decode) + { + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = ((float)((const unsigned char*)input_data)[input_pixel_index + c]) / 255; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_uchar_to_linear_float[((const unsigned char*)input_data)[input_pixel_index + c]]; + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = ((float)((const unsigned char*)input_data)[input_pixel_index + alpha_channel]) / 255; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = ((float)((const unsigned short*)input_data)[input_pixel_index + c]) / 65535; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_to_linear(((float)((const unsigned short*)input_data)[input_pixel_index + c]) / 65535); + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = ((float)((const unsigned short*)input_data)[input_pixel_index + alpha_channel]) / 65535; + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = (float)(((double)((const unsigned int*)input_data)[input_pixel_index + c]) / 4294967295); + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_to_linear((float)(((double)((const unsigned int*)input_data)[input_pixel_index + c]) / 4294967295)); + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = (float)(((double)((const unsigned int*)input_data)[input_pixel_index + alpha_channel]) / 4294967295); + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_LINEAR): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = ((const float*)input_data)[input_pixel_index + c]; + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_SRGB): + for (; x < max_x; x++) + { + int decode_pixel_index = x * channels; + int input_pixel_index = stbir__edge_wrap(edge_horizontal, x, input_w) * channels; + for (c = 0; c < channels; c++) + decode_buffer[decode_pixel_index + c] = stbir__srgb_to_linear(((const float*)input_data)[input_pixel_index + c]); + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + decode_buffer[decode_pixel_index + alpha_channel] = ((const float*)input_data)[input_pixel_index + alpha_channel]; + } + + break; + + default: + STBIR__UNIMPLEMENTED("Unknown type/colorspace/channels combination."); + break; + } + + if (!(stbir_info->flags & STBIR_FLAG_ALPHA_PREMULTIPLIED)) + { + for (x = -stbir_info->horizontal_filter_pixel_margin; x < max_x; x++) + { + int decode_pixel_index = x * channels; + + // If the alpha value is 0 it will clobber the color values. Make sure it's not. + float alpha = decode_buffer[decode_pixel_index + alpha_channel]; +#ifndef STBIR_NO_ALPHA_EPSILON + if (stbir_info->type != STBIR_TYPE_FLOAT) { + alpha += STBIR_ALPHA_EPSILON; + decode_buffer[decode_pixel_index + alpha_channel] = alpha; + } +#endif + for (c = 0; c < channels; c++) + { + if (c == alpha_channel) + continue; + + decode_buffer[decode_pixel_index + c] *= alpha; + } + } + } + + if (edge_horizontal == STBIR_EDGE_ZERO) + { + for (x = -stbir_info->horizontal_filter_pixel_margin; x < 0; x++) + { + for (c = 0; c < channels; c++) + decode_buffer[x*channels + c] = 0; + } + for (x = input_w; x < max_x; x++) + { + for (c = 0; c < channels; c++) + decode_buffer[x*channels + c] = 0; + } + } +} + +static float* stbir__get_ring_buffer_entry(float* ring_buffer, int index, int ring_buffer_length) +{ + return &ring_buffer[index * ring_buffer_length]; +} + +static float* stbir__add_empty_ring_buffer_entry(stbir__info* stbir_info, int n) +{ + int ring_buffer_index; + float* ring_buffer; + + if (stbir_info->ring_buffer_begin_index < 0) + { + ring_buffer_index = stbir_info->ring_buffer_begin_index = 0; + stbir_info->ring_buffer_first_scanline = n; + } + else + { + ring_buffer_index = (stbir_info->ring_buffer_begin_index + (stbir_info->ring_buffer_last_scanline - stbir_info->ring_buffer_first_scanline) + 1) % stbir_info->vertical_filter_pixel_width; + STBIR__DEBUG_ASSERT(ring_buffer_index != stbir_info->ring_buffer_begin_index); + } + + ring_buffer = stbir__get_ring_buffer_entry(stbir_info->ring_buffer, ring_buffer_index, stbir_info->ring_buffer_length_bytes / sizeof(float)); + memset(ring_buffer, 0, stbir_info->ring_buffer_length_bytes); + + stbir_info->ring_buffer_last_scanline = n; + + return ring_buffer; +} + + +static void stbir__resample_horizontal_upsample(stbir__info* stbir_info, int n, float* output_buffer) +{ + int x, k; + int output_w = stbir_info->output_w; + int kernel_pixel_width = stbir_info->horizontal_filter_pixel_width; + int channels = stbir_info->channels; + float* decode_buffer = stbir__get_decode_buffer(stbir_info); + stbir__contributors* horizontal_contributors = stbir_info->horizontal_contributors; + float* horizontal_coefficients = stbir_info->horizontal_coefficients; + int coefficient_width = stbir_info->horizontal_coefficient_width; + + for (x = 0; x < output_w; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int out_pixel_index = x * channels; + int coefficient_group = coefficient_width * x; + int coefficient_counter = 0; + + STBIR__DEBUG_ASSERT(n1 >= n0); + STBIR__DEBUG_ASSERT(n0 >= -stbir_info->horizontal_filter_pixel_margin); + STBIR__DEBUG_ASSERT(n1 >= -stbir_info->horizontal_filter_pixel_margin); + STBIR__DEBUG_ASSERT(n0 < stbir_info->input_w + stbir_info->horizontal_filter_pixel_margin); + STBIR__DEBUG_ASSERT(n1 < stbir_info->input_w + stbir_info->horizontal_filter_pixel_margin); + + switch (channels) { + case 1: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 1; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + } + break; + case 2: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 2; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + } + break; + case 3: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 3; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + } + break; + case 4: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * 4; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + output_buffer[out_pixel_index + 3] += decode_buffer[in_pixel_index + 3] * coefficient; + } + break; + default: + for (k = n0; k <= n1; k++) + { + int in_pixel_index = k * channels; + float coefficient = horizontal_coefficients[coefficient_group + coefficient_counter++]; + int c; + STBIR__DEBUG_ASSERT(coefficient != 0); + for (c = 0; c < channels; c++) + output_buffer[out_pixel_index + c] += decode_buffer[in_pixel_index + c] * coefficient; + } + break; + } + } +} + +static void stbir__resample_horizontal_downsample(stbir__info* stbir_info, int n, float* output_buffer) +{ + int x, k; + int input_w = stbir_info->input_w; + int output_w = stbir_info->output_w; + int kernel_pixel_width = stbir_info->horizontal_filter_pixel_width; + int channels = stbir_info->channels; + float* decode_buffer = stbir__get_decode_buffer(stbir_info); + stbir__contributors* horizontal_contributors = stbir_info->horizontal_contributors; + float* horizontal_coefficients = stbir_info->horizontal_coefficients; + int coefficient_width = stbir_info->horizontal_coefficient_width; + int filter_pixel_margin = stbir_info->horizontal_filter_pixel_margin; + int max_x = input_w + filter_pixel_margin * 2; + + STBIR__DEBUG_ASSERT(!stbir__use_width_upsampling(stbir_info)); + + switch (channels) { + case 1: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 1; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 1; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + } + } + break; + + case 2: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 2; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 2; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + } + } + break; + + case 3: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 3; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 3; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + } + } + break; + + case 4: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * 4; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int out_pixel_index = k * 4; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR__DEBUG_ASSERT(coefficient != 0); + output_buffer[out_pixel_index + 0] += decode_buffer[in_pixel_index + 0] * coefficient; + output_buffer[out_pixel_index + 1] += decode_buffer[in_pixel_index + 1] * coefficient; + output_buffer[out_pixel_index + 2] += decode_buffer[in_pixel_index + 2] * coefficient; + output_buffer[out_pixel_index + 3] += decode_buffer[in_pixel_index + 3] * coefficient; + } + } + break; + + default: + for (x = 0; x < max_x; x++) + { + int n0 = horizontal_contributors[x].n0; + int n1 = horizontal_contributors[x].n1; + + int in_x = x - filter_pixel_margin; + int in_pixel_index = in_x * channels; + int max_n = n1; + int coefficient_group = coefficient_width * x; + + for (k = n0; k <= max_n; k++) + { + int c; + int out_pixel_index = k * channels; + float coefficient = horizontal_coefficients[coefficient_group + k - n0]; + STBIR__DEBUG_ASSERT(coefficient != 0); + for (c = 0; c < channels; c++) + output_buffer[out_pixel_index + c] += decode_buffer[in_pixel_index + c] * coefficient; + } + } + break; + } +} + +static void stbir__decode_and_resample_upsample(stbir__info* stbir_info, int n) +{ + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline(stbir_info, n); + + // Now resample it into the ring buffer. + if (stbir__use_width_upsampling(stbir_info)) + stbir__resample_horizontal_upsample(stbir_info, n, stbir__add_empty_ring_buffer_entry(stbir_info, n)); + else + stbir__resample_horizontal_downsample(stbir_info, n, stbir__add_empty_ring_buffer_entry(stbir_info, n)); + + // Now it's sitting in the ring buffer ready to be used as source for the vertical sampling. +} + +static void stbir__decode_and_resample_downsample(stbir__info* stbir_info, int n) +{ + // Decode the nth scanline from the source image into the decode buffer. + stbir__decode_scanline(stbir_info, n); + + memset(stbir_info->horizontal_buffer, 0, stbir_info->output_w * stbir_info->channels * sizeof(float)); + + // Now resample it into the horizontal buffer. + if (stbir__use_width_upsampling(stbir_info)) + stbir__resample_horizontal_upsample(stbir_info, n, stbir_info->horizontal_buffer); + else + stbir__resample_horizontal_downsample(stbir_info, n, stbir_info->horizontal_buffer); + + // Now it's sitting in the horizontal buffer ready to be distributed into the ring buffers. +} + +// Get the specified scan line from the ring buffer. +static float* stbir__get_ring_buffer_scanline(int get_scanline, float* ring_buffer, int begin_index, int first_scanline, int ring_buffer_size, int ring_buffer_length) +{ + int ring_buffer_index = (begin_index + (get_scanline - first_scanline)) % ring_buffer_size; + return stbir__get_ring_buffer_entry(ring_buffer, ring_buffer_index, ring_buffer_length); +} + + +static void stbir__encode_scanline(stbir__info* stbir_info, int num_pixels, void *output_buffer, float *encode_buffer, int channels, int alpha_channel, int decode) +{ + int x; + int n; + int num_nonalpha; + stbir_uint16 nonalpha[STBIR_MAX_CHANNELS]; + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_PREMULTIPLIED)) + { + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + float alpha = encode_buffer[pixel_index + alpha_channel]; + float reciprocal_alpha = alpha ? 1.0f / alpha : 0; + + // unrolling this produced a 1% slowdown upscaling a large RGBA linear-space image on my machine - stb + for (n = 0; n < channels; n++) + if (n != alpha_channel) + encode_buffer[pixel_index + n] *= reciprocal_alpha; + + // We added in a small epsilon to prevent the color channel from being deleted with zero alpha. + // Because we only add it for integer types, it will automatically be discarded on integer + // conversion, so we don't need to subtract it back out (which would be problematic for + // numeric precision reasons). + } + } + + // build a table of all channels that need colorspace correction, so + // we don't perform colorspace correction on channels that don't need it. + for (x=0, num_nonalpha=0; x < channels; ++x) + if (x != alpha_channel || (stbir_info->flags & STBIR_FLAG_ALPHA_USES_COLORSPACE)) + nonalpha[num_nonalpha++] = x; + + #define STBIR__ROUND_INT(f) ((int) ((f)+0.5)) + #define STBIR__ROUND_UINT(f) ((stbir_uint32) ((f)+0.5)) + + #ifdef STBIR__SATURATE_INT + #define STBIR__ENCODE_LINEAR8(f) stbir__saturate8 (STBIR__ROUND_INT((f) * 255 )) + #define STBIR__ENCODE_LINEAR16(f) stbir__saturate16(STBIR__ROUND_INT((f) * 65535)) + #else + #define STBIR__ENCODE_LINEAR8(f) (unsigned char ) STBIR__ROUND_INT(stbir__saturate(f) * 255 ) + #define STBIR__ENCODE_LINEAR16(f) (unsigned short) STBIR__ROUND_INT(stbir__saturate(f) * 65535) + #endif + + switch (decode) + { + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((unsigned char*)output_buffer)[index] = STBIR__ENCODE_LINEAR8(encode_buffer[index]); + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT8, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((unsigned char*)output_buffer)[index] = stbir__linear_to_srgb_uchar(encode_buffer[index]); + } + + if (!(stbir_info->flags & STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((unsigned char *)output_buffer)[pixel_index + alpha_channel] = STBIR__ENCODE_LINEAR8(encode_buffer[pixel_index+alpha_channel]); + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((unsigned short*)output_buffer)[index] = STBIR__ENCODE_LINEAR16(encode_buffer[index]); + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT16, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((unsigned short*)output_buffer)[index] = (unsigned short)STBIR__ROUND_INT(stbir__linear_to_srgb(stbir__saturate(encode_buffer[index])) * 65535); + } + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((unsigned short*)output_buffer)[pixel_index + alpha_channel] = STBIR__ENCODE_LINEAR16(encode_buffer[pixel_index + alpha_channel]); + } + + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((unsigned int*)output_buffer)[index] = (unsigned int)STBIR__ROUND_UINT(((double)stbir__saturate(encode_buffer[index])) * 4294967295); + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_UINT32, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((unsigned int*)output_buffer)[index] = (unsigned int)STBIR__ROUND_UINT(((double)stbir__linear_to_srgb(stbir__saturate(encode_buffer[index]))) * 4294967295); + } + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((unsigned int*)output_buffer)[pixel_index + alpha_channel] = (unsigned int)STBIR__ROUND_INT(((double)stbir__saturate(encode_buffer[pixel_index + alpha_channel])) * 4294967295); + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_LINEAR): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < channels; n++) + { + int index = pixel_index + n; + ((float*)output_buffer)[index] = encode_buffer[index]; + } + } + break; + + case STBIR__DECODE(STBIR_TYPE_FLOAT, STBIR_COLORSPACE_SRGB): + for (x=0; x < num_pixels; ++x) + { + int pixel_index = x*channels; + + for (n = 0; n < num_nonalpha; n++) + { + int index = pixel_index + nonalpha[n]; + ((float*)output_buffer)[index] = stbir__linear_to_srgb(encode_buffer[index]); + } + + if (!(stbir_info->flags&STBIR_FLAG_ALPHA_USES_COLORSPACE)) + ((float*)output_buffer)[pixel_index + alpha_channel] = encode_buffer[pixel_index + alpha_channel]; + } + break; + + default: + STBIR__UNIMPLEMENTED("Unknown type/colorspace/channels combination."); + break; + } +} + +static void stbir__resample_vertical_upsample(stbir__info* stbir_info, int n, int in_first_scanline, int in_last_scanline, float in_center_of_out) +{ + int x, k; + int output_w = stbir_info->output_w; + stbir__contributors* vertical_contributors = stbir_info->vertical_contributors; + float* vertical_coefficients = stbir_info->vertical_coefficients; + int channels = stbir_info->channels; + int alpha_channel = stbir_info->alpha_channel; + int type = stbir_info->type; + int colorspace = stbir_info->colorspace; + int kernel_pixel_width = stbir_info->vertical_filter_pixel_width; + void* output_data = stbir_info->output_data; + float* encode_buffer = stbir_info->encode_buffer; + int decode = STBIR__DECODE(type, colorspace); + int coefficient_width = stbir_info->vertical_coefficient_width; + int coefficient_counter; + int contributor = n; + + float* ring_buffer = stbir_info->ring_buffer; + int ring_buffer_begin_index = stbir_info->ring_buffer_begin_index; + int ring_buffer_first_scanline = stbir_info->ring_buffer_first_scanline; + int ring_buffer_last_scanline = stbir_info->ring_buffer_last_scanline; + int ring_buffer_length = stbir_info->ring_buffer_length_bytes/sizeof(float); + + int n0,n1, output_row_start; + int coefficient_group = coefficient_width * contributor; + + n0 = vertical_contributors[contributor].n0; + n1 = vertical_contributors[contributor].n1; + + output_row_start = n * stbir_info->output_stride_bytes; + + STBIR__DEBUG_ASSERT(stbir__use_height_upsampling(stbir_info)); + + memset(encode_buffer, 0, output_w * sizeof(float) * channels); + + // I tried reblocking this for better cache usage of encode_buffer + // (using x_outer, k, x_inner), but it lost speed. -- stb + + coefficient_counter = 0; + switch (channels) { + case 1: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, kernel_pixel_width, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 1; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + } + } + break; + case 2: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, kernel_pixel_width, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 2; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + encode_buffer[in_pixel_index + 1] += ring_buffer_entry[in_pixel_index + 1] * coefficient; + } + } + break; + case 3: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, kernel_pixel_width, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 3; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + encode_buffer[in_pixel_index + 1] += ring_buffer_entry[in_pixel_index + 1] * coefficient; + encode_buffer[in_pixel_index + 2] += ring_buffer_entry[in_pixel_index + 2] * coefficient; + } + } + break; + case 4: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, kernel_pixel_width, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * 4; + encode_buffer[in_pixel_index + 0] += ring_buffer_entry[in_pixel_index + 0] * coefficient; + encode_buffer[in_pixel_index + 1] += ring_buffer_entry[in_pixel_index + 1] * coefficient; + encode_buffer[in_pixel_index + 2] += ring_buffer_entry[in_pixel_index + 2] * coefficient; + encode_buffer[in_pixel_index + 3] += ring_buffer_entry[in_pixel_index + 3] * coefficient; + } + } + break; + default: + for (k = n0; k <= n1; k++) + { + int coefficient_index = coefficient_counter++; + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, kernel_pixel_width, ring_buffer_length); + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + for (x = 0; x < output_w; ++x) + { + int in_pixel_index = x * channels; + int c; + for (c = 0; c < channels; c++) + encode_buffer[in_pixel_index + c] += ring_buffer_entry[in_pixel_index + c] * coefficient; + } + } + break; + } + stbir__encode_scanline(stbir_info, output_w, (char *) output_data + output_row_start, encode_buffer, channels, alpha_channel, decode); +} + +static void stbir__resample_vertical_downsample(stbir__info* stbir_info, int n, int in_first_scanline, int in_last_scanline, float in_center_of_out) +{ + int x, k; + int output_w = stbir_info->output_w; + int output_h = stbir_info->output_h; + stbir__contributors* vertical_contributors = stbir_info->vertical_contributors; + float* vertical_coefficients = stbir_info->vertical_coefficients; + int channels = stbir_info->channels; + int kernel_pixel_width = stbir_info->vertical_filter_pixel_width; + void* output_data = stbir_info->output_data; + float* horizontal_buffer = stbir_info->horizontal_buffer; + int coefficient_width = stbir_info->vertical_coefficient_width; + int contributor = n + stbir_info->vertical_filter_pixel_margin; + + float* ring_buffer = stbir_info->ring_buffer; + int ring_buffer_begin_index = stbir_info->ring_buffer_begin_index; + int ring_buffer_first_scanline = stbir_info->ring_buffer_first_scanline; + int ring_buffer_last_scanline = stbir_info->ring_buffer_last_scanline; + int ring_buffer_length = stbir_info->ring_buffer_length_bytes/sizeof(float); + int n0,n1; + + n0 = vertical_contributors[contributor].n0; + n1 = vertical_contributors[contributor].n1; + + STBIR__DEBUG_ASSERT(!stbir__use_height_upsampling(stbir_info)); + + for (k = n0; k <= n1; k++) + { + int coefficient_index = k - n0; + int coefficient_group = coefficient_width * contributor; + float coefficient = vertical_coefficients[coefficient_group + coefficient_index]; + + float* ring_buffer_entry = stbir__get_ring_buffer_scanline(k, ring_buffer, ring_buffer_begin_index, ring_buffer_first_scanline, kernel_pixel_width, ring_buffer_length); + + switch (channels) { + case 1: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 1; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + } + break; + case 2: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 2; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + ring_buffer_entry[in_pixel_index + 1] += horizontal_buffer[in_pixel_index + 1] * coefficient; + } + break; + case 3: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 3; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + ring_buffer_entry[in_pixel_index + 1] += horizontal_buffer[in_pixel_index + 1] * coefficient; + ring_buffer_entry[in_pixel_index + 2] += horizontal_buffer[in_pixel_index + 2] * coefficient; + } + break; + case 4: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * 4; + ring_buffer_entry[in_pixel_index + 0] += horizontal_buffer[in_pixel_index + 0] * coefficient; + ring_buffer_entry[in_pixel_index + 1] += horizontal_buffer[in_pixel_index + 1] * coefficient; + ring_buffer_entry[in_pixel_index + 2] += horizontal_buffer[in_pixel_index + 2] * coefficient; + ring_buffer_entry[in_pixel_index + 3] += horizontal_buffer[in_pixel_index + 3] * coefficient; + } + break; + default: + for (x = 0; x < output_w; x++) + { + int in_pixel_index = x * channels; + + int c; + for (c = 0; c < channels; c++) + ring_buffer_entry[in_pixel_index + c] += horizontal_buffer[in_pixel_index + c] * coefficient; + } + break; + } + } +} + +static void stbir__buffer_loop_upsample(stbir__info* stbir_info) +{ + int y; + float scale_ratio = stbir_info->vertical_scale; + float out_scanlines_radius = stbir__filter_info_table[stbir_info->vertical_filter].support(1/scale_ratio) * scale_ratio; + + STBIR__DEBUG_ASSERT(stbir__use_height_upsampling(stbir_info)); + + for (y = 0; y < stbir_info->output_h; y++) + { + float in_center_of_out = 0; // Center of the current out scanline in the in scanline space + int in_first_scanline = 0, in_last_scanline = 0; + + stbir__calculate_sample_range_upsample(y, out_scanlines_radius, scale_ratio, stbir_info->vertical_shift, &in_first_scanline, &in_last_scanline, &in_center_of_out); + + STBIR__DEBUG_ASSERT(in_last_scanline - in_first_scanline <= stbir_info->vertical_filter_pixel_width); + + if (stbir_info->ring_buffer_begin_index >= 0) + { + // Get rid of whatever we don't need anymore. + while (in_first_scanline > stbir_info->ring_buffer_first_scanline) + { + if (stbir_info->ring_buffer_first_scanline == stbir_info->ring_buffer_last_scanline) + { + // We just popped the last scanline off the ring buffer. + // Reset it to the empty state. + stbir_info->ring_buffer_begin_index = -1; + stbir_info->ring_buffer_first_scanline = 0; + stbir_info->ring_buffer_last_scanline = 0; + break; + } + else + { + stbir_info->ring_buffer_first_scanline++; + stbir_info->ring_buffer_begin_index = (stbir_info->ring_buffer_begin_index + 1) % stbir_info->vertical_filter_pixel_width; + } + } + } + + // Load in new ones. + if (stbir_info->ring_buffer_begin_index < 0) + stbir__decode_and_resample_upsample(stbir_info, in_first_scanline); + + while (in_last_scanline > stbir_info->ring_buffer_last_scanline) + stbir__decode_and_resample_upsample(stbir_info, stbir_info->ring_buffer_last_scanline + 1); + + // Now all buffers should be ready to write a row of vertical sampling. + stbir__resample_vertical_upsample(stbir_info, y, in_first_scanline, in_last_scanline, in_center_of_out); + + STBIR_PROGRESS_REPORT((float)y / stbir_info->output_h); + } +} + +static void stbir__empty_ring_buffer(stbir__info* stbir_info, int first_necessary_scanline) +{ + int output_stride_bytes = stbir_info->output_stride_bytes; + int channels = stbir_info->channels; + int alpha_channel = stbir_info->alpha_channel; + int type = stbir_info->type; + int colorspace = stbir_info->colorspace; + int output_w = stbir_info->output_w; + void* output_data = stbir_info->output_data; + int decode = STBIR__DECODE(type, colorspace); + + float* ring_buffer = stbir_info->ring_buffer; + int ring_buffer_length = stbir_info->ring_buffer_length_bytes/sizeof(float); + + if (stbir_info->ring_buffer_begin_index >= 0) + { + // Get rid of whatever we don't need anymore. + while (first_necessary_scanline > stbir_info->ring_buffer_first_scanline) + { + if (stbir_info->ring_buffer_first_scanline >= 0 && stbir_info->ring_buffer_first_scanline < stbir_info->output_h) + { + int output_row_start = stbir_info->ring_buffer_first_scanline * output_stride_bytes; + float* ring_buffer_entry = stbir__get_ring_buffer_entry(ring_buffer, stbir_info->ring_buffer_begin_index, ring_buffer_length); + stbir__encode_scanline(stbir_info, output_w, (char *) output_data + output_row_start, ring_buffer_entry, channels, alpha_channel, decode); + STBIR_PROGRESS_REPORT((float)stbir_info->ring_buffer_first_scanline / stbir_info->output_h); + } + + if (stbir_info->ring_buffer_first_scanline == stbir_info->ring_buffer_last_scanline) + { + // We just popped the last scanline off the ring buffer. + // Reset it to the empty state. + stbir_info->ring_buffer_begin_index = -1; + stbir_info->ring_buffer_first_scanline = 0; + stbir_info->ring_buffer_last_scanline = 0; + break; + } + else + { + stbir_info->ring_buffer_first_scanline++; + stbir_info->ring_buffer_begin_index = (stbir_info->ring_buffer_begin_index + 1) % stbir_info->vertical_filter_pixel_width; + } + } + } +} + +static void stbir__buffer_loop_downsample(stbir__info* stbir_info) +{ + int y; + float scale_ratio = stbir_info->vertical_scale; + int output_h = stbir_info->output_h; + float in_pixels_radius = stbir__filter_info_table[stbir_info->vertical_filter].support(scale_ratio) / scale_ratio; + int pixel_margin = stbir_info->vertical_filter_pixel_margin; + int max_y = stbir_info->input_h + pixel_margin; + + STBIR__DEBUG_ASSERT(!stbir__use_height_upsampling(stbir_info)); + + for (y = -pixel_margin; y < max_y; y++) + { + float out_center_of_in; // Center of the current out scanline in the in scanline space + int out_first_scanline, out_last_scanline; + + stbir__calculate_sample_range_downsample(y, in_pixels_radius, scale_ratio, stbir_info->vertical_shift, &out_first_scanline, &out_last_scanline, &out_center_of_in); + + STBIR__DEBUG_ASSERT(out_last_scanline - out_first_scanline <= stbir_info->vertical_filter_pixel_width); + + if (out_last_scanline < 0 || out_first_scanline >= output_h) + continue; + + stbir__empty_ring_buffer(stbir_info, out_first_scanline); + + stbir__decode_and_resample_downsample(stbir_info, y); + + // Load in new ones. + if (stbir_info->ring_buffer_begin_index < 0) + stbir__add_empty_ring_buffer_entry(stbir_info, out_first_scanline); + + while (out_last_scanline > stbir_info->ring_buffer_last_scanline) + stbir__add_empty_ring_buffer_entry(stbir_info, stbir_info->ring_buffer_last_scanline + 1); + + // Now the horizontal buffer is ready to write to all ring buffer rows. + stbir__resample_vertical_downsample(stbir_info, y, out_first_scanline, out_last_scanline, out_center_of_in); + } + + stbir__empty_ring_buffer(stbir_info, stbir_info->output_h); +} + +static void stbir__setup(stbir__info *info, int input_w, int input_h, int output_w, int output_h, int channels) +{ + info->input_w = input_w; + info->input_h = input_h; + info->output_w = output_w; + info->output_h = output_h; + info->channels = channels; +} + +static void stbir__calculate_transform(stbir__info *info, float s0, float t0, float s1, float t1, float *transform) +{ + info->s0 = s0; + info->t0 = t0; + info->s1 = s1; + info->t1 = t1; + + if (transform) + { + info->horizontal_scale = transform[0]; + info->vertical_scale = transform[1]; + info->horizontal_shift = transform[2]; + info->vertical_shift = transform[3]; + } + else + { + info->horizontal_scale = ((float)info->output_w / info->input_w) / (s1 - s0); + info->vertical_scale = ((float)info->output_h / info->input_h) / (t1 - t0); + + info->horizontal_shift = s0 * info->input_w / (s1 - s0); + info->vertical_shift = t0 * info->input_h / (t1 - t0); + } +} + +static void stbir__choose_filter(stbir__info *info, stbir_filter h_filter, stbir_filter v_filter) +{ + if (h_filter == 0) + h_filter = stbir__use_upsampling(info->horizontal_scale) ? STBIR_DEFAULT_FILTER_UPSAMPLE : STBIR_DEFAULT_FILTER_DOWNSAMPLE; + if (v_filter == 0) + v_filter = stbir__use_upsampling(info->vertical_scale) ? STBIR_DEFAULT_FILTER_UPSAMPLE : STBIR_DEFAULT_FILTER_DOWNSAMPLE; + info->horizontal_filter = h_filter; + info->vertical_filter = v_filter; +} + +static stbir_uint32 stbir__calculate_memory(stbir__info *info) +{ + int pixel_margin = stbir__get_filter_pixel_margin(info->horizontal_filter, info->horizontal_scale); + int filter_height = stbir__get_filter_pixel_width(info->vertical_filter, info->vertical_scale); + + info->horizontal_num_contributors = stbir__get_contributors(info->horizontal_scale, info->horizontal_filter, info->input_w, info->output_w); + info->vertical_num_contributors = stbir__get_contributors(info->vertical_scale , info->vertical_filter , info->input_h, info->output_h); + + info->horizontal_contributors_size = info->horizontal_num_contributors * sizeof(stbir__contributors); + info->horizontal_coefficients_size = stbir__get_total_horizontal_coefficients(info) * sizeof(float); + info->vertical_contributors_size = info->vertical_num_contributors * sizeof(stbir__contributors); + info->vertical_coefficients_size = stbir__get_total_vertical_coefficients(info) * sizeof(float); + info->decode_buffer_size = (info->input_w + pixel_margin * 2) * info->channels * sizeof(float); + info->horizontal_buffer_size = info->output_w * info->channels * sizeof(float); + info->ring_buffer_size = info->output_w * info->channels * filter_height * sizeof(float); + info->encode_buffer_size = info->output_w * info->channels * sizeof(float); + + STBIR_ASSERT(info->horizontal_filter != 0); + STBIR_ASSERT(info->horizontal_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); // this now happens too late + STBIR_ASSERT(info->vertical_filter != 0); + STBIR_ASSERT(info->vertical_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); // this now happens too late + + if (stbir__use_height_upsampling(info)) + // The horizontal buffer is for when we're downsampling the height and we + // can't output the result of sampling the decode buffer directly into the + // ring buffers. + info->horizontal_buffer_size = 0; + else + // The encode buffer is to retain precision in the height upsampling method + // and isn't used when height downsampling. + info->encode_buffer_size = 0; + + return info->horizontal_contributors_size + info->horizontal_coefficients_size + + info->vertical_contributors_size + info->vertical_coefficients_size + + info->decode_buffer_size + info->horizontal_buffer_size + + info->ring_buffer_size + info->encode_buffer_size; +} + +static int stbir__resize_allocated(stbir__info *info, + const void* input_data, int input_stride_in_bytes, + void* output_data, int output_stride_in_bytes, + int alpha_channel, stbir_uint32 flags, stbir_datatype type, + stbir_edge edge_horizontal, stbir_edge edge_vertical, stbir_colorspace colorspace, + void* tempmem, size_t tempmem_size_in_bytes) +{ + size_t memory_required = stbir__calculate_memory(info); + + int width_stride_input = input_stride_in_bytes ? input_stride_in_bytes : info->channels * info->input_w * stbir__type_size[type]; + int width_stride_output = output_stride_in_bytes ? output_stride_in_bytes : info->channels * info->output_w * stbir__type_size[type]; + +#ifdef STBIR_DEBUG_OVERWRITE_TEST +#define OVERWRITE_ARRAY_SIZE 8 + unsigned char overwrite_output_before_pre[OVERWRITE_ARRAY_SIZE]; + unsigned char overwrite_tempmem_before_pre[OVERWRITE_ARRAY_SIZE]; + unsigned char overwrite_output_after_pre[OVERWRITE_ARRAY_SIZE]; + unsigned char overwrite_tempmem_after_pre[OVERWRITE_ARRAY_SIZE]; + + size_t begin_forbidden = width_stride_output * (info->output_h - 1) + info->output_w * info->channels * stbir__type_size[type]; + memcpy(overwrite_output_before_pre, &((unsigned char*)output_data)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE); + memcpy(overwrite_output_after_pre, &((unsigned char*)output_data)[begin_forbidden], OVERWRITE_ARRAY_SIZE); + memcpy(overwrite_tempmem_before_pre, &((unsigned char*)tempmem)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE); + memcpy(overwrite_tempmem_after_pre, &((unsigned char*)tempmem)[tempmem_size_in_bytes], OVERWRITE_ARRAY_SIZE); +#endif + + STBIR_ASSERT(info->channels >= 0); + STBIR_ASSERT(info->channels <= STBIR_MAX_CHANNELS); + + if (info->channels < 0 || info->channels > STBIR_MAX_CHANNELS) + return 0; + + STBIR_ASSERT(info->horizontal_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); + STBIR_ASSERT(info->vertical_filter < STBIR__ARRAY_SIZE(stbir__filter_info_table)); + + if (info->horizontal_filter >= STBIR__ARRAY_SIZE(stbir__filter_info_table)) + return 0; + if (info->vertical_filter >= STBIR__ARRAY_SIZE(stbir__filter_info_table)) + return 0; + + if (alpha_channel < 0) + flags |= STBIR_FLAG_ALPHA_USES_COLORSPACE | STBIR_FLAG_ALPHA_PREMULTIPLIED; + + if (!(flags&STBIR_FLAG_ALPHA_USES_COLORSPACE) || !(flags&STBIR_FLAG_ALPHA_PREMULTIPLIED)) + STBIR_ASSERT(alpha_channel >= 0 && alpha_channel < info->channels); + + if (alpha_channel >= info->channels) + return 0; + + STBIR_ASSERT(tempmem); + + if (!tempmem) + return 0; + + STBIR_ASSERT(tempmem_size_in_bytes >= memory_required); + + if (tempmem_size_in_bytes < memory_required) + return 0; + + memset(tempmem, 0, tempmem_size_in_bytes); + + info->input_data = input_data; + info->input_stride_bytes = width_stride_input; + + info->output_data = output_data; + info->output_stride_bytes = width_stride_output; + + info->alpha_channel = alpha_channel; + info->flags = flags; + info->type = type; + info->edge_horizontal = edge_horizontal; + info->edge_vertical = edge_vertical; + info->colorspace = colorspace; + + info->horizontal_coefficient_width = stbir__get_coefficient_width (info->horizontal_filter, info->horizontal_scale); + info->vertical_coefficient_width = stbir__get_coefficient_width (info->vertical_filter , info->vertical_scale ); + info->horizontal_filter_pixel_width = stbir__get_filter_pixel_width (info->horizontal_filter, info->horizontal_scale); + info->vertical_filter_pixel_width = stbir__get_filter_pixel_width (info->vertical_filter , info->vertical_scale ); + info->horizontal_filter_pixel_margin = stbir__get_filter_pixel_margin(info->horizontal_filter, info->horizontal_scale); + info->vertical_filter_pixel_margin = stbir__get_filter_pixel_margin(info->vertical_filter , info->vertical_scale ); + + info->ring_buffer_length_bytes = info->output_w * info->channels * sizeof(float); + info->decode_buffer_pixels = info->input_w + info->horizontal_filter_pixel_margin * 2; + +#define STBIR__NEXT_MEMPTR(current, newtype) (newtype*)(((unsigned char*)current) + current##_size) + + info->horizontal_contributors = (stbir__contributors *) tempmem; + info->horizontal_coefficients = STBIR__NEXT_MEMPTR(info->horizontal_contributors, float); + info->vertical_contributors = STBIR__NEXT_MEMPTR(info->horizontal_coefficients, stbir__contributors); + info->vertical_coefficients = STBIR__NEXT_MEMPTR(info->vertical_contributors, float); + info->decode_buffer = STBIR__NEXT_MEMPTR(info->vertical_coefficients, float); + + if (stbir__use_height_upsampling(info)) + { + info->horizontal_buffer = NULL; + info->ring_buffer = STBIR__NEXT_MEMPTR(info->decode_buffer, float); + info->encode_buffer = STBIR__NEXT_MEMPTR(info->ring_buffer, float); + + STBIR__DEBUG_ASSERT((size_t)STBIR__NEXT_MEMPTR(info->encode_buffer, unsigned char) == (size_t)tempmem + tempmem_size_in_bytes); + } + else + { + info->horizontal_buffer = STBIR__NEXT_MEMPTR(info->decode_buffer, float); + info->ring_buffer = STBIR__NEXT_MEMPTR(info->horizontal_buffer, float); + info->encode_buffer = NULL; + + STBIR__DEBUG_ASSERT((size_t)STBIR__NEXT_MEMPTR(info->ring_buffer, unsigned char) == (size_t)tempmem + tempmem_size_in_bytes); + } + +#undef STBIR__NEXT_MEMPTR + + // This signals that the ring buffer is empty + info->ring_buffer_begin_index = -1; + + stbir__calculate_filters(info, info->horizontal_contributors, info->horizontal_coefficients, info->horizontal_filter, info->horizontal_scale, info->horizontal_shift, info->input_w, info->output_w); + stbir__calculate_filters(info, info->vertical_contributors, info->vertical_coefficients, info->vertical_filter, info->vertical_scale, info->vertical_shift, info->input_h, info->output_h); + + STBIR_PROGRESS_REPORT(0); + + if (stbir__use_height_upsampling(info)) + stbir__buffer_loop_upsample(info); + else + stbir__buffer_loop_downsample(info); + + STBIR_PROGRESS_REPORT(1); + +#ifdef STBIR_DEBUG_OVERWRITE_TEST + STBIR__DEBUG_ASSERT(memcmp(overwrite_output_before_pre, &((unsigned char*)output_data)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE) == 0); + STBIR__DEBUG_ASSERT(memcmp(overwrite_output_after_pre, &((unsigned char*)output_data)[begin_forbidden], OVERWRITE_ARRAY_SIZE) == 0); + STBIR__DEBUG_ASSERT(memcmp(overwrite_tempmem_before_pre, &((unsigned char*)tempmem)[-OVERWRITE_ARRAY_SIZE], OVERWRITE_ARRAY_SIZE) == 0); + STBIR__DEBUG_ASSERT(memcmp(overwrite_tempmem_after_pre, &((unsigned char*)tempmem)[tempmem_size_in_bytes], OVERWRITE_ARRAY_SIZE) == 0); +#endif + + return 1; +} + + +static int stbir__resize_arbitrary( + void *alloc_context, + const void* input_data, int input_w, int input_h, int input_stride_in_bytes, + void* output_data, int output_w, int output_h, int output_stride_in_bytes, + float s0, float t0, float s1, float t1, float *transform, + int channels, int alpha_channel, stbir_uint32 flags, stbir_datatype type, + stbir_filter h_filter, stbir_filter v_filter, + stbir_edge edge_horizontal, stbir_edge edge_vertical, stbir_colorspace colorspace) +{ + stbir__info info; + int result; + size_t memory_required; + void* extra_memory; + + stbir__setup(&info, input_w, input_h, output_w, output_h, channels); + stbir__calculate_transform(&info, s0,t0,s1,t1,transform); + stbir__choose_filter(&info, h_filter, v_filter); + memory_required = stbir__calculate_memory(&info); + extra_memory = STBIR_MALLOC(memory_required, alloc_context); + + if (!extra_memory) + return 0; + + result = stbir__resize_allocated(&info, input_data, input_stride_in_bytes, + output_data, output_stride_in_bytes, + alpha_channel, flags, type, + edge_horizontal, edge_vertical, + colorspace, extra_memory, memory_required); + + STBIR_FREE(extra_memory, alloc_context); + + return result; +} + +STBIRDEF int stbir_resize_uint8( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,-1,0, STBIR_TYPE_UINT8, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_COLORSPACE_LINEAR); +} + +STBIRDEF int stbir_resize_float( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,-1,0, STBIR_TYPE_FLOAT, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_COLORSPACE_LINEAR); +} + +STBIRDEF int stbir_resize_uint8_srgb(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT8, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + STBIR_EDGE_CLAMP, STBIR_EDGE_CLAMP, STBIR_COLORSPACE_SRGB); +} + +STBIRDEF int stbir_resize_uint8_srgb_edgemode(const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode) +{ + return stbir__resize_arbitrary(NULL, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT8, STBIR_FILTER_DEFAULT, STBIR_FILTER_DEFAULT, + edge_wrap_mode, edge_wrap_mode, STBIR_COLORSPACE_SRGB); +} + +STBIRDEF int stbir_resize_uint8_generic( const unsigned char *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + unsigned char *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT8, filter, filter, + edge_wrap_mode, edge_wrap_mode, space); +} + +STBIRDEF int stbir_resize_uint16_generic(const stbir_uint16 *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + stbir_uint16 *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_UINT16, filter, filter, + edge_wrap_mode, edge_wrap_mode, space); +} + + +STBIRDEF int stbir_resize_float_generic( const float *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + float *output_pixels , int output_w, int output_h, int output_stride_in_bytes, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_wrap_mode, stbir_filter filter, stbir_colorspace space, + void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, STBIR_TYPE_FLOAT, filter, filter, + edge_wrap_mode, edge_wrap_mode, space); +} + + +STBIRDEF int stbir_resize( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,NULL,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical, + edge_mode_horizontal, edge_mode_vertical, space); +} + + +STBIRDEF int stbir_resize_subpixel(const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float x_scale, float y_scale, + float x_offset, float y_offset) +{ + float transform[4]; + transform[0] = x_scale; + transform[1] = y_scale; + transform[2] = x_offset; + transform[3] = y_offset; + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + 0,0,1,1,transform,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical, + edge_mode_horizontal, edge_mode_vertical, space); +} + +STBIRDEF int stbir_resize_region( const void *input_pixels , int input_w , int input_h , int input_stride_in_bytes, + void *output_pixels, int output_w, int output_h, int output_stride_in_bytes, + stbir_datatype datatype, + int num_channels, int alpha_channel, int flags, + stbir_edge edge_mode_horizontal, stbir_edge edge_mode_vertical, + stbir_filter filter_horizontal, stbir_filter filter_vertical, + stbir_colorspace space, void *alloc_context, + float s0, float t0, float s1, float t1) +{ + return stbir__resize_arbitrary(alloc_context, input_pixels, input_w, input_h, input_stride_in_bytes, + output_pixels, output_w, output_h, output_stride_in_bytes, + s0,t0,s1,t1,NULL,num_channels,alpha_channel,flags, datatype, filter_horizontal, filter_vertical, + edge_mode_horizontal, edge_mode_vertical, space); +} + +#endif // STB_IMAGE_RESIZE_IMPLEMENTATION \ No newline at end of file diff --git a/Tests/Tests.cpp b/Tests/Tests.cpp index b9858440a..129c33b4f 100644 --- a/Tests/Tests.cpp +++ b/Tests/Tests.cpp @@ -11,9 +11,9 @@ void runTests(PluginEditor* editor) std::thread testRunnerThread([editor] { ObjectFuzzTest objectFuzzer(editor); HelpFileFuzzTest helpfileFuzzer(editor); - + UnitTestRunner runner; - runner.runTests({&objectFuzzer, &helpfileFuzzer}, 1); + runner.runTests({&objectFuzzer, &helpfileFuzzer}, 23); }); testRunnerThread.detach(); }