All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.itextpdf.layout.renderer.RootRenderer Maven / Gradle / Ivy

There is a newer version: 9.0.0
Show newest version
/*
    This file is part of the iText (R) project.
    Copyright (c) 1998-2023 Apryse Group NV
    Authors: Apryse Software.

    This program is offered under a commercial and under the AGPL license.
    For commercial licensing, contact us at https://itextpdf.com/sales.  For AGPL licensing, see below.

    AGPL licensing:
    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU Affero General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU Affero General Public License for more details.

    You should have received a copy of the GNU Affero General Public License
    along with this program.  If not, see .
 */
package com.itextpdf.layout.renderer;

import com.itextpdf.io.logs.IoLogMessageConstant;
import com.itextpdf.commons.utils.MessageFormatUtil;
import com.itextpdf.commons.actions.EventManager;
import com.itextpdf.kernel.actions.events.LinkDocumentIdEvent;
import com.itextpdf.commons.actions.sequence.AbstractIdentifiableElement;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.PdfDocument;
import com.itextpdf.layout.IPropertyContainer;
import com.itextpdf.layout.layout.LayoutArea;
import com.itextpdf.layout.layout.LayoutContext;
import com.itextpdf.layout.layout.LayoutPosition;
import com.itextpdf.layout.layout.LayoutResult;
import com.itextpdf.layout.layout.PositionedLayoutContext;
import com.itextpdf.layout.layout.RootLayoutArea;
import com.itextpdf.layout.logs.LayoutLogMessageConstant;
import com.itextpdf.layout.margincollapse.MarginsCollapseHandler;
import com.itextpdf.layout.margincollapse.MarginsCollapseInfo;
import com.itextpdf.layout.properties.ClearPropertyValue;
import com.itextpdf.layout.properties.Property;
import com.itextpdf.layout.tagging.LayoutTaggingHelper;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class RootRenderer extends AbstractRenderer {

    protected boolean immediateFlush = true;
    protected RootLayoutArea currentArea;
    protected List waitingDrawingElements = new ArrayList<>();
    List floatRendererAreas;
    private IRenderer keepWithNextHangingRenderer;
    private LayoutResult keepWithNextHangingRendererLayoutResult;
    private MarginsCollapseHandler marginsCollapseHandler;
    private LayoutArea initialCurrentArea;
    private List waitingNextPageRenderers = new ArrayList<>();
    private boolean floatOverflowedCompletely = false;

    public void addChild(IRenderer renderer) {
        LayoutTaggingHelper taggingHelper = this.getProperty(Property.TAGGING_HELPER);
        if (taggingHelper != null) {
            LayoutTaggingHelper.addTreeHints(taggingHelper, renderer);
        }

        // Some positioned renderers might have been fetched from non-positioned child and added to this renderer,
        // so we use this generic mechanism of determining which renderers have been just added.
        int numberOfChildRenderers = childRenderers.size();
        int numberOfPositionedChildRenderers = positionedRenderers.size();
        super.addChild(renderer);
        List addedRenderers = new ArrayList<>(1);
        List addedPositionedRenderers = new ArrayList<>(1);
        while (childRenderers.size() > numberOfChildRenderers) {
            addedRenderers.add(childRenderers.get(numberOfChildRenderers));
            childRenderers.remove(numberOfChildRenderers);
        }
        while (positionedRenderers.size() > numberOfPositionedChildRenderers) {
            addedPositionedRenderers.add(positionedRenderers.get(numberOfPositionedChildRenderers));
            positionedRenderers.remove(numberOfPositionedChildRenderers);
        }

        boolean marginsCollapsingEnabled = Boolean.TRUE.equals(getPropertyAsBoolean(Property.COLLAPSING_MARGINS));
        if (currentArea == null) {
            updateCurrentAndInitialArea(null);
            if (marginsCollapsingEnabled) {
                marginsCollapseHandler = new MarginsCollapseHandler(this, null);
            }
        }

        // Static layout
        for (int i = 0; currentArea != null && i < addedRenderers.size(); i++) {
            RootRendererAreaStateHandler rootRendererStateHandler = new RootRendererAreaStateHandler();

            renderer = addedRenderers.get(i);
            boolean rendererIsFloat = FloatingHelper.isRendererFloating(renderer);
            boolean clearanceOverflowsToNextPage = FloatingHelper.isClearanceApplied(waitingNextPageRenderers, renderer.getProperty(Property.CLEAR));
            if (rendererIsFloat && (floatOverflowedCompletely || clearanceOverflowsToNextPage)) {
                waitingNextPageRenderers.add(renderer);
                floatOverflowedCompletely = true;
                continue;
            }

            processWaitingKeepWithNextElement(renderer);

            List resultRenderers = new ArrayList<>();
            LayoutResult result = null;

            MarginsCollapseInfo childMarginsInfo = null;
            if (marginsCollapsingEnabled && currentArea != null && renderer != null) {
                childMarginsInfo = marginsCollapseHandler.startChildMarginsHandling(renderer, currentArea.getBBox());
            }
            while (clearanceOverflowsToNextPage || currentArea != null && renderer != null
                        && (result = renderer.setParent(this)
                            .layout(new LayoutContext(currentArea.clone(), childMarginsInfo, floatRendererAreas))).getStatus() != LayoutResult.FULL) {
                boolean currentAreaNeedsToBeUpdated = false;
                if (clearanceOverflowsToNextPage) {
                    result = new LayoutResult(LayoutResult.NOTHING, null, null, renderer);
                    currentAreaNeedsToBeUpdated = true;
                }
                if (result.getStatus() == LayoutResult.PARTIAL) {
                    if (rendererIsFloat) {
                        waitingNextPageRenderers.add(result.getOverflowRenderer());
                        break;
                    } else {
                        processRenderer(result.getSplitRenderer(), resultRenderers);
                        if (!rootRendererStateHandler.attemptGoForwardToStoredNextState(this)) {
                            currentAreaNeedsToBeUpdated = true;
                        }
                    }
                } else if (result.getStatus() == LayoutResult.NOTHING && !clearanceOverflowsToNextPage) {
                    if (result.getOverflowRenderer() instanceof ImageRenderer) {
                        float imgHeight = ((ImageRenderer) result.getOverflowRenderer()).getOccupiedArea().getBBox().getHeight();
                        if (!floatRendererAreas.isEmpty()
                                || currentArea.getBBox().getHeight() < imgHeight && !currentArea.isEmptyArea()) {
                            if (rendererIsFloat) {
                                waitingNextPageRenderers.add(result.getOverflowRenderer());
                                floatOverflowedCompletely = true;
                                break;
                            }
                            currentAreaNeedsToBeUpdated = true;
                        } else {
                            ((ImageRenderer) result.getOverflowRenderer()).autoScale(currentArea);
                            result.getOverflowRenderer().setProperty(Property.FORCED_PLACEMENT, true);
                            Logger logger = LoggerFactory.getLogger(RootRenderer.class);
                            logger.warn(MessageFormatUtil.format(LayoutLogMessageConstant.ELEMENT_DOES_NOT_FIT_AREA, ""));
                        }
                    } else {
                        if (currentArea.isEmptyArea() && result.getAreaBreak() == null) {
                            boolean keepTogetherChanged = tryDisableKeepTogether(result,
                                    rendererIsFloat, rootRendererStateHandler);

                            boolean areKeepTogetherAndForcedPlacementBothNotChanged = !keepTogetherChanged;
                            if (areKeepTogetherAndForcedPlacementBothNotChanged) {
                                areKeepTogetherAndForcedPlacementBothNotChanged =
                                        ! updateForcedPlacement(renderer, result.getOverflowRenderer());
                            }

                            if (areKeepTogetherAndForcedPlacementBothNotChanged) {
                                // FORCED_PLACEMENT was already set to the renderer and
                                // LogMessageConstant.ELEMENT_DOES_NOT_FIT_AREA message was logged.
                                // This else-clause should never be hit, otherwise there is a bug in FORCED_PLACEMENT implementation.
                                assert false;

                                // Still handling this case in order to avoid nasty infinite loops.
                                break;
                            }
                        } else {
                            rootRendererStateHandler.storePreviousState(this);
                            if (!rootRendererStateHandler.attemptGoForwardToStoredNextState(this)) {
                                if (rendererIsFloat) {
                                    waitingNextPageRenderers.add(result.getOverflowRenderer());
                                    floatOverflowedCompletely = true;
                                    break;
                                }
                                currentAreaNeedsToBeUpdated = true;
                            }
                        }
                    }
                }

                renderer = result.getOverflowRenderer();

                if (marginsCollapsingEnabled) {
                    marginsCollapseHandler.endChildMarginsHandling(currentArea.getBBox());
                }
                if (currentAreaNeedsToBeUpdated) {
                    updateCurrentAndInitialArea(result);
                }
                if (marginsCollapsingEnabled) {
                    marginsCollapseHandler = new MarginsCollapseHandler(this, null);
                    childMarginsInfo = marginsCollapseHandler.startChildMarginsHandling(renderer, currentArea.getBBox());
                }

                clearanceOverflowsToNextPage = clearanceOverflowsToNextPage
                        && FloatingHelper.isClearanceApplied(waitingNextPageRenderers, renderer.getProperty(Property.CLEAR));
            }
            if (marginsCollapsingEnabled) {
                marginsCollapseHandler.endChildMarginsHandling(currentArea.getBBox());
            }

            if (null != result && null != result.getSplitRenderer()) {
                renderer = result.getSplitRenderer();
            }

            // Keep renderer until next element is added for future keep with next adjustments
            if (renderer != null && result != null) {
                if (Boolean.TRUE.equals(renderer.getProperty(Property.KEEP_WITH_NEXT))) {
                    if (Boolean.TRUE.equals(renderer.getProperty(Property.FORCED_PLACEMENT))) {
                        Logger logger = LoggerFactory.getLogger(RootRenderer.class);
                        logger.warn(IoLogMessageConstant.ELEMENT_WAS_FORCE_PLACED_KEEP_WITH_NEXT_WILL_BE_IGNORED);
                        shrinkCurrentAreaAndProcessRenderer(renderer, resultRenderers, result);
                    } else {
                        keepWithNextHangingRenderer = renderer;
                        keepWithNextHangingRendererLayoutResult = result;
                    }
                } else if (result.getStatus() != LayoutResult.NOTHING) {
                    shrinkCurrentAreaAndProcessRenderer(renderer, resultRenderers, result);
                }
            }
        }

        for (int i = 0; i < addedPositionedRenderers.size(); i++) {
            positionedRenderers.add(addedPositionedRenderers.get(i));
            renderer = positionedRenderers.get(positionedRenderers.size() - 1);
            Integer positionedPageNumber = renderer.getProperty(Property.PAGE_NUMBER);
            if (positionedPageNumber == null) {
                positionedPageNumber = currentArea.getPageNumber();
            }

            LayoutArea layoutArea;
            // For position=absolute, if none of the top, bottom, left, right properties are provided,
            // the content should be displayed in the flow of the current content, not overlapping it.
            // The behavior is just if it would be statically positioned except it does not affect other elements
            if (Integer.valueOf(LayoutPosition.ABSOLUTE).equals(renderer.getProperty(Property.POSITION)) && AbstractRenderer.noAbsolutePositionInfo(renderer)) {
                layoutArea = new LayoutArea((int) positionedPageNumber, currentArea.getBBox().clone());
            } else {
                layoutArea = new LayoutArea((int) positionedPageNumber, initialCurrentArea.getBBox().clone());
            }
            Rectangle fullBbox = layoutArea.getBBox().clone();
            preparePositionedRendererAndAreaForLayout(renderer, fullBbox, layoutArea.getBBox());
            renderer.layout(new PositionedLayoutContext(new LayoutArea(layoutArea.getPageNumber(), fullBbox), layoutArea));

            if (immediateFlush) {
                flushSingleRenderer(renderer);
                positionedRenderers.remove(positionedRenderers.size() - 1);
            }
        }
    }

    /**
     * Draws (flushes) the content.
     *
     * @see #draw(com.itextpdf.layout.renderer.DrawContext)
     */
    public void flush() {
        for (IRenderer resultRenderer : childRenderers) {
            flushSingleRenderer(resultRenderer);
        }
        for (IRenderer resultRenderer : positionedRenderers) {
            flushSingleRenderer(resultRenderer);
        }
        childRenderers.clear();
        positionedRenderers.clear();
    }

    /**
     * This method correctly closes the {@link RootRenderer} instance.
     * There might be hanging elements, like in case of {@link Property#KEEP_WITH_NEXT} is set to true
     * and when no consequent element has been added. This method addresses such situations.
     */
    public void close() {
        addAllWaitingNextPageRenderers();
        if (keepWithNextHangingRenderer != null) {
            keepWithNextHangingRenderer.setProperty(Property.KEEP_WITH_NEXT, false);
            IRenderer rendererToBeAdded = keepWithNextHangingRenderer;
            keepWithNextHangingRenderer = null;
            addChild(rendererToBeAdded);
        }
        if (!immediateFlush) {
            flush();
        }
        flushWaitingDrawingElements(true);
        LayoutTaggingHelper taggingHelper = this.getProperty(Property.TAGGING_HELPER);
        if (taggingHelper != null) {
            taggingHelper.releaseAllHints();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public LayoutResult layout(LayoutContext layoutContext) {
        throw new IllegalStateException("Layout is not supported for root renderers.");
    }

    public LayoutArea getCurrentArea() {
        if (currentArea == null) {
            updateCurrentAndInitialArea(null);
        }
        return currentArea;
    }

    protected abstract void flushSingleRenderer(IRenderer resultRenderer);

    protected abstract LayoutArea updateCurrentArea(LayoutResult overflowResult);

    protected void shrinkCurrentAreaAndProcessRenderer(IRenderer renderer, List resultRenderers, LayoutResult result) {
        if (currentArea != null) {
            float resultRendererHeight = result.getOccupiedArea().getBBox().getHeight();
            currentArea.getBBox().setHeight(currentArea.getBBox().getHeight() - resultRendererHeight);
            if (currentArea.isEmptyArea() && (resultRendererHeight > 0 || FloatingHelper.isRendererFloating(renderer))) {
                currentArea.setEmptyArea(false);
            }
            processRenderer(renderer, resultRenderers);
        }

        if (!immediateFlush) {
            childRenderers.addAll(resultRenderers);
        }
    }

    protected void flushWaitingDrawingElements() {
        flushWaitingDrawingElements(true);
    }

    void flushWaitingDrawingElements(boolean force) {
        Set flushedElements = new HashSet<>();
        for (int i = 0; i < waitingDrawingElements.size(); ++i)
        {
            IRenderer waitingDrawingElement = waitingDrawingElements.get(i);
            // TODO Remove checking occupied area to be not null when DEVSIX-1655 is resolved.
            if (force || (null != waitingDrawingElement.getOccupiedArea() && waitingDrawingElement.getOccupiedArea().getPageNumber() < currentArea.getPageNumber())) {
                flushSingleRenderer(waitingDrawingElement);
                flushedElements.add(waitingDrawingElement);
            } else if (null == waitingDrawingElement.getOccupiedArea()) {
                flushedElements.add(waitingDrawingElement);
            }
        }
        waitingDrawingElements.removeAll(flushedElements);
    }

    final void linkRenderToDocument(IRenderer renderer, PdfDocument pdfDocument) {
        if (renderer == null) {
            return;
        }
        final IPropertyContainer container = renderer.getModelElement();
        if (container instanceof AbstractIdentifiableElement) {
            EventManager.getInstance().onEvent(
                    new LinkDocumentIdEvent(pdfDocument, (AbstractIdentifiableElement) container)
            );
        }
        final List children = renderer.getChildRenderers();
        if (children != null) {
            for (IRenderer child : children) {
                linkRenderToDocument(child, pdfDocument);
            }
        }
    }

    private void processRenderer(IRenderer renderer, List resultRenderers) {
        alignChildHorizontally(renderer, currentArea.getBBox());
        if (immediateFlush) {
            flushSingleRenderer(renderer);
        } else {
            resultRenderers.add(renderer);
        }
    }

    private void processWaitingKeepWithNextElement(IRenderer renderer) {
        if (keepWithNextHangingRenderer != null) {
            LayoutArea rest = currentArea.clone();
            rest.getBBox().setHeight(rest.getBBox().getHeight() - keepWithNextHangingRendererLayoutResult.getOccupiedArea().getBBox().getHeight());
            boolean ableToProcessKeepWithNext = false;
            if (renderer.setParent(this).layout(new LayoutContext(rest)).getStatus() != LayoutResult.NOTHING) {
                // The area break will not be introduced and we are safe to place everything as is
                shrinkCurrentAreaAndProcessRenderer(keepWithNextHangingRenderer, new ArrayList(), keepWithNextHangingRendererLayoutResult);
                ableToProcessKeepWithNext = true;
            } else {
                float originalElementHeight = keepWithNextHangingRendererLayoutResult.getOccupiedArea().getBBox().getHeight();
                List trySplitHeightPoints = new ArrayList<>();
                float delta = 35;
                for (int i = 1; i <= 5 && originalElementHeight - delta * i > originalElementHeight / 2; i++) {
                    trySplitHeightPoints.add(originalElementHeight - delta * i);
                }
                for (int i = 0; i < trySplitHeightPoints.size() && !ableToProcessKeepWithNext; i++) {
                    float curElementSplitHeight = trySplitHeightPoints.get(i);
                    RootLayoutArea firstElementSplitLayoutArea = (RootLayoutArea) currentArea.clone();
                    firstElementSplitLayoutArea.getBBox().setHeight(curElementSplitHeight).
                            moveUp(currentArea.getBBox().getHeight() - curElementSplitHeight);
                    LayoutResult firstElementSplitLayoutResult = keepWithNextHangingRenderer.setParent(this).layout(new LayoutContext(firstElementSplitLayoutArea.clone()));
                    if (firstElementSplitLayoutResult.getStatus() == LayoutResult.PARTIAL) {
                        RootLayoutArea storedArea = currentArea;
                        updateCurrentAndInitialArea(firstElementSplitLayoutResult);
                        LayoutResult firstElementOverflowLayoutResult = firstElementSplitLayoutResult.getOverflowRenderer().layout(new LayoutContext(currentArea.clone()));
                        if (firstElementOverflowLayoutResult.getStatus() == LayoutResult.FULL) {
                            LayoutArea secondElementLayoutArea = currentArea.clone();
                            secondElementLayoutArea.getBBox().setHeight(secondElementLayoutArea.getBBox().getHeight() - firstElementOverflowLayoutResult.getOccupiedArea().getBBox().getHeight());
                            LayoutResult secondElementLayoutResult = renderer.setParent(this).layout(new LayoutContext(secondElementLayoutArea));
                            if (secondElementLayoutResult.getStatus() != LayoutResult.NOTHING) {
                                ableToProcessKeepWithNext = true;

                                currentArea = firstElementSplitLayoutArea;
                                shrinkCurrentAreaAndProcessRenderer(firstElementSplitLayoutResult.getSplitRenderer(), new ArrayList(), firstElementSplitLayoutResult);
                                updateCurrentAndInitialArea(firstElementSplitLayoutResult);
                                shrinkCurrentAreaAndProcessRenderer(firstElementSplitLayoutResult.getOverflowRenderer(), new ArrayList(), firstElementOverflowLayoutResult);
                            }
                        }
                        if (!ableToProcessKeepWithNext) {
                            currentArea = storedArea;
                        }
                    }
                }
            }
            if (!ableToProcessKeepWithNext && !currentArea.isEmptyArea()) {
                RootLayoutArea storedArea = currentArea;
                updateCurrentAndInitialArea(null);
                LayoutResult firstElementLayoutResult = keepWithNextHangingRenderer.setParent(this).layout(new LayoutContext(currentArea.clone()));
                if (firstElementLayoutResult.getStatus() == LayoutResult.FULL) {
                    LayoutArea secondElementLayoutArea = currentArea.clone();
                    secondElementLayoutArea.getBBox().setHeight(secondElementLayoutArea.getBBox().getHeight() - firstElementLayoutResult.getOccupiedArea().getBBox().getHeight());
                    LayoutResult secondElementLayoutResult = renderer.setParent(this).layout(new LayoutContext(secondElementLayoutArea));
                    if (secondElementLayoutResult.getStatus() != LayoutResult.NOTHING) {
                        ableToProcessKeepWithNext = true;
                        shrinkCurrentAreaAndProcessRenderer(keepWithNextHangingRenderer, new ArrayList(), keepWithNextHangingRendererLayoutResult);
                    }
                }
                if (!ableToProcessKeepWithNext) {
                    currentArea = storedArea;
                }
            }
            if (!ableToProcessKeepWithNext) {
                Logger logger = LoggerFactory.getLogger(RootRenderer.class);
                logger.warn(IoLogMessageConstant.RENDERER_WAS_NOT_ABLE_TO_PROCESS_KEEP_WITH_NEXT);
                shrinkCurrentAreaAndProcessRenderer(keepWithNextHangingRenderer, new ArrayList(), keepWithNextHangingRendererLayoutResult);
            }
            keepWithNextHangingRenderer = null;
            keepWithNextHangingRendererLayoutResult = null;
        }
    }

    private void updateCurrentAndInitialArea(LayoutResult overflowResult) {
        floatRendererAreas = new ArrayList<>();
        updateCurrentArea(overflowResult);
        initialCurrentArea = currentArea == null ? null : currentArea.clone();
        addWaitingNextPageRenderers();
    }

    private void addAllWaitingNextPageRenderers() {
        boolean marginsCollapsingEnabled = Boolean.TRUE.equals(getPropertyAsBoolean(Property.COLLAPSING_MARGINS));
        while (!waitingNextPageRenderers.isEmpty()) {
            if (marginsCollapsingEnabled) {
                marginsCollapseHandler = new MarginsCollapseHandler(this, null);
            }
            updateCurrentAndInitialArea(null);
        }
    }

    private void addWaitingNextPageRenderers() {
        floatOverflowedCompletely = false;
        List waitingFloatRenderers = new ArrayList<>(waitingNextPageRenderers);
        waitingNextPageRenderers.clear();
        for (IRenderer renderer : waitingFloatRenderers) {
            addChild(renderer);
        }
    }

    private boolean updateForcedPlacement(IRenderer currentRenderer, IRenderer overflowRenderer) {
        if (Boolean.TRUE.equals(currentRenderer.getProperty(Property.FORCED_PLACEMENT))) {
            return false;
        } else {
            overflowRenderer.setProperty(Property.FORCED_PLACEMENT, true);
            Logger logger = LoggerFactory.getLogger(RootRenderer.class);
            if (logger.isWarnEnabled()) {
                logger.warn(MessageFormatUtil.format(LayoutLogMessageConstant.ELEMENT_DOES_NOT_FIT_AREA, ""));
            }
            return true;
        }
    }

    private boolean tryDisableKeepTogether(LayoutResult result,
            boolean rendererIsFloat, RootRendererAreaStateHandler rootRendererStateHandler) {
        IRenderer toDisableKeepTogether = null;

        // looking for the most outer keep together element
        IRenderer current = result.getCauseOfNothing();
        while (current != null) {
            if (Boolean.TRUE.equals(current.getProperty(Property.KEEP_TOGETHER))) {
                toDisableKeepTogether = current;
            }
            current = current.getParent();
        }

        if (toDisableKeepTogether == null) {
            return false;
        }

        // Ideally the disabling of keep together property should be done on the renderers layer,
        // but due to the problem with renderers tree (parent links from causeOfNothing
        // may not lead to overflowRenderer) such approach does not work now. So we are
        // disabling keep together on the models layer.
        toDisableKeepTogether.getModelElement().setProperty(Property.KEEP_TOGETHER, false);
        Logger logger = LoggerFactory.getLogger(RootRenderer.class);
        if (logger.isWarnEnabled()) {
            logger.warn(MessageFormatUtil.format(
                    LayoutLogMessageConstant.ELEMENT_DOES_NOT_FIT_AREA,
                    "KeepTogether property will be ignored."));
        }
        if (!rendererIsFloat) {
            rootRendererStateHandler.attemptGoBackToStoredPreviousStateAndStoreNextState(this);
        }
        return true;
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy